diff --git a/.eslintrc.json b/.eslintrc.json
index 8d20e22..95cba7d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,6 @@
{
"extends": "./node_modules/mwts/",
- "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
+ "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings", "scripts"],
"env": {
"jest": true
}
diff --git a/.gitignore b/.gitignore
index cd12d03..e2c1f24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,8 @@ run/
yarn.lock
**/config.prod.ts
**/config.local.ts
-container
\ No newline at end of file
+container
+scripts
+ai
+tmp_uploads/
+.trae
\ No newline at end of file
diff --git a/area-api-doc.md b/area-api-doc.md
new file mode 100644
index 0000000..6dd8f8e
--- /dev/null
+++ b/area-api-doc.md
@@ -0,0 +1,254 @@
+# Area 区域管理 API 文档
+
+## 概述
+
+本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能.
+
+## API 接口列表
+
+### 1. 创建区域
+
+**请求信息**
+- URL: `/api/area`
+- Method: `POST`
+- Headers: `Authorization: Bearer {token}`
+
+**请求体 (JSON)**
+```json
+{
+ "name": "欧洲",
+ "latitude": 48.8566,
+ "longitude": 2.3522
+}
+```
+
+**参数说明**
+- `name`: 区域名称 (必填)
+- `latitude`: 纬度 (-90 到 90 之间,可选)
+- `longitude`: 经度 (-180 到 180 之间,可选)
+
+**响应示例**
+```json
+{
+ "code": 0,
+ "message": "创建成功",
+ "data": {
+ "id": 1,
+ "name": "欧洲",
+ "latitude": 48.8566,
+ "longitude": 2.3522,
+ "createdAt": "2024-01-01T12:00:00Z",
+ "updatedAt": "2024-01-01T12:00:00Z"
+ }
+}
+```
+
+### 2. 更新区域
+
+**请求信息**
+- URL: `/api/area/{id}`
+- Method: `PUT`
+- Headers: `Authorization: Bearer {token}`
+
+**请求体 (JSON)**
+```json
+{
+ "name": "欧洲区域",
+ "latitude": 48.8566,
+ "longitude": 2.3522
+}
+```
+
+**参数说明**
+- `name`: 区域名称 (可选)
+- `latitude`: 纬度 (-90 到 90 之间,可选)
+- `longitude`: 经度 (-180 到 180 之间,可选)
+
+**响应示例**
+```json
+{
+ "code": 0,
+ "message": "更新成功",
+ "data": {
+ "id": 1,
+ "name": "欧洲区域",
+ "latitude": 48.8566,
+ "longitude": 2.3522,
+ "createdAt": "2024-01-01T12:00:00Z",
+ "updatedAt": "2024-01-01T12:30:00Z"
+ }
+}
+```
+
+### 3. 删除区域
+
+**请求信息**
+- URL: `/api/area/{id}`
+- Method: `DELETE`
+- Headers: `Authorization: Bearer {token}`
+
+**响应示例**
+```json
+{
+ "code": 0,
+ "message": "删除成功",
+ "data": null
+}
+```
+
+### 4. 获取区域列表(分页)
+
+**请求信息**
+- URL: `/api/area`
+- Method: `GET`
+- Headers: `Authorization: Bearer {token}`
+- Query Parameters:
+ - `currentPage`: 当前页码 (默认 1)
+ - `pageSize`: 每页数量 (默认 10)
+ - `name`: 区域名称(可选,用于搜索)
+
+**响应示例**
+```json
+{
+ "code": 0,
+ "message": "查询成功",
+ "data": {
+ "list": [
+ {
+ "id": 1,
+ "name": "欧洲",
+ "latitude": 48.8566,
+ "longitude": 2.3522,
+ "createdAt": "2024-01-01T12:00:00Z",
+ "updatedAt": "2024-01-01T12:00:00Z"
+ }
+ ],
+ "total": 1
+ }
+}
+```
+
+### 5. 获取所有区域
+
+**请求信息**
+- URL: `/api/area/all`
+- Method: `GET`
+- Headers: `Authorization: Bearer {token}`
+
+**响应示例**
+```json
+{
+ "code": 0,
+ "message": "查询成功",
+ "data": [
+ {
+ "id": 1,
+ "name": "欧洲",
+ "latitude": 48.8566,
+ "longitude": 2.3522,
+ "createdAt": "2024-01-01T12:00:00Z",
+ "updatedAt": "2024-01-01T12:00:00Z"
+ },
+ {
+ "id": 2,
+ "name": "亚洲",
+ "latitude": 35.6762,
+ "longitude": 139.6503,
+ "createdAt": "2024-01-01T12:10:00Z",
+ "updatedAt": "2024-01-01T12:10:00Z"
+ }
+ ]
+}
+```
+
+### 6. 根据ID获取区域详情
+
+**请求信息**
+- URL: `/api/area/{id}`
+- Method: `GET`
+- Headers: `Authorization: Bearer {token}`
+
+**响应示例**
+```json
+{
+ "code": 0,
+ "message": "查询成功",
+ "data": {
+ "id": 1,
+ "name": "欧洲",
+ "latitude": 48.8566,
+ "longitude": 2.3522,
+ "createdAt": "2024-01-01T12:00:00Z",
+ "updatedAt": "2024-01-01T12:00:00Z"
+ }
+}
+```
+
+## 世界地图实现建议
+
+对于前端实现世界地图并显示区域坐标,推荐以下方案:
+
+### 1. 使用开源地图库
+
+- **Leaflet.js**: 轻量级开源地图库,易于集成
+- **Mapbox**: 提供丰富的地图样式和交互功能
+- **Google Maps API**: 功能强大但需要API密钥
+
+### 2. 实现步骤
+
+1. **获取区域数据**:
+ 使用 `/api/area/all` 接口获取所有包含坐标信息的区域
+
+2. **初始化地图**:
+ ```javascript
+ // Leaflet示例
+ const map = L.map('map').setView([0, 0], 2);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors'
+ }).addTo(map);
+ ```
+
+3. **添加区域标记**:
+ ```javascript
+ // 假设areas是从API获取的数据
+ areas.forEach(area => {
+ if (area.latitude && area.longitude) {
+ const marker = L.marker([area.latitude, area.longitude]).addTo(map);
+ marker.bindPopup(`${area.name}`);
+ }
+ });
+ ```
+
+4. **添加交互功能**:
+ - 点击标记显示区域详情
+ - 搜索和筛选功能
+ - 编辑坐标功能(调用更新API)
+
+### 3. 坐标输入建议
+
+在区域管理界面,可以添加以下功能来辅助坐标输入:
+
+1. 提供搜索框,根据地点名称自动获取坐标
+2. 集成小型地图,允许用户点击选择位置
+3. 添加验证,确保输入的坐标在有效范围内
+
+## 数据模型说明
+
+### Area 实体
+
+| 字段名 | 类型 | 描述 | 是否必填 |
+|--------|------|------|----------|
+| id | number | 区域ID | 否(自动生成) |
+| name | string | 区域名称 | 是 |
+| latitude | number | 纬度(范围:-90 到 90) | 否 |
+| longitude | number | 经度(范围:-180 到 180) | 否 |
+| createdAt | Date | 创建时间 | 否(自动生成) |
+| updatedAt | Date | 更新时间 | 否(自动生成) |
+
+## 错误处理
+
+API可能返回的错误信息:
+
+- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
+- `区域不存在`: 当尝试更新或删除不存在的区域时
+- `权限错误`: 当请求缺少有效的授权令牌时
\ 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/migration-guide.md b/migration-guide.md
new file mode 100644
index 0000000..4bb0666
--- /dev/null
+++ b/migration-guide.md
@@ -0,0 +1,49 @@
+# 数据库迁移指南
+
+为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude` 和 `longitude` 字段添加到数据库表中.
+
+## 执行迁移步骤
+
+### 1. 生成迁移文件
+
+运行以下命令生成迁移文件:
+
+```bash
+npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
+```
+
+### 2. 检查迁移文件
+
+生成的迁移文件会自动包含添加 `latitude` 和 `longitude` 字段的SQL语句.您可以检查文件内容确保迁移逻辑正确.
+
+### 3. 执行迁移
+
+运行以下命令执行迁移,将更改应用到数据库:
+
+```bash
+npm run migration:run
+```
+
+## 手动迁移SQL(可选)
+
+如果需要手动执行SQL,可以使用以下语句:
+
+```sql
+ALTER TABLE `area`
+ADD COLUMN `latitude` DECIMAL(10,6) NULL AFTER `name`,
+ADD COLUMN `longitude` DECIMAL(10,6) NULL AFTER `latitude`;
+```
+
+## 回滚迁移(如需)
+
+如果遇到问题,可以使用以下命令回滚迁移:
+
+```bash
+npm run typeorm -- migration:revert -d src/db/datasource.ts
+```
+
+## 注意事项
+
+- 确保在执行迁移前备份数据库
+- 迁移不会影响现有数据,新增字段默认为 NULL
+- 迁移后,可以通过API开始为区域添加坐标信息
\ No newline at end of file
diff --git a/output.log b/output.log
new file mode 100644
index 0000000..db350f0
--- /dev/null
+++ b/output.log
@@ -0,0 +1,24 @@
+
+> my-midway-project@1.0.0 dev
+> cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js
+
+[2J[3J[H
+[[90m10:37:17 AM[0m] Starting compilation in watch mode...
+
+
+
+
+
+
+[[90m10:37:19 AM[0m] Found 0 errors. Watching for file changes.
+
+2025-12-01 10:37:20.106 INFO 58678 [SyncProductJob] start job SyncProductJob
+2025-12-01 10:37:20.106 INFO 58678 [SyncShipmentJob] start job SyncShipmentJob
+2025-12-01 10:37:20.109 INFO 58678 [SyncProductJob] complete job SyncProductJob
+
+[32mNode.js server[0m [2mstarted in[0m 732 ms
+
+[32m➜[0m Local: [36mhttp://127.0.0.1:[1m7001[0m[36m/[0m[0m
+[32m➜[0m [2mNetwork: http://192.168.5.100:7001/ [0m
+
+2025-12-01 10:37:20.110 INFO 58678 [SyncShipmentJob] complete job SyncShipmentJob
diff --git a/package-lock.json b/package-lock.json
index 0ede98f..e207cd1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,21 +20,33 @@
"@midwayjs/logger": "^3.1.0",
"@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0",
+ "@midwayjs/upload": "^3.20.16",
"@midwayjs/validate": "^3.20.2",
- "axios": "^1.7.9",
+ "@woocommerce/woocommerce-rest-api": "^1.0.2",
+ "axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
+ "csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
- "mysql2": "^3.11.5",
+ "eta": "^4.4.1",
+ "i18n-iso-countries": "^7.14.0",
+ "mysql2": "^3.15.3",
"nodemailer": "^7.0.5",
+ "npm-check-updates": "^19.1.2",
+ "qs": "^6.14.0",
+ "sharp": "^0.33.3",
"swagger-ui-dist": "^5.18.2",
- "typeorm": "^0.3.20",
+ "typeorm": "^0.3.27",
+ "typeorm-extension": "^3.7.2",
+ "wpapi": "^1.2.2",
+ "xlsx": "^0.18.5",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@midwayjs/mock": "^3.20.11",
"cross-env": "^10.1.0",
"mwtsc": "^1.15.2",
+ "tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"engines": {
@@ -52,6 +64,16 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
+ "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz",
@@ -59,6 +81,465 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@faker-js/faker": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
+ "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
+ "npm": ">=10"
+ }
+ },
"node_modules/@hapi/bourne": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",
@@ -80,6 +561,367 @@
"@hapi/hoek": "^9.0.0"
}
},
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -347,6 +1189,19 @@
"node": ">=12"
}
},
+ "node_modules/@midwayjs/upload": {
+ "version": "3.20.16",
+ "resolved": "https://registry.npmjs.org/@midwayjs/upload/-/upload-3.20.16.tgz",
+ "integrity": "sha512-lnHDOeU4wGvABJjYjSR5dpG+6f4+hxJhU+1TsT+OsrKMDju6AyTOnTSFa1V1vMO7+VplKzkWP+ZFJkAVI0Buuw==",
+ "license": "MIT",
+ "dependencies": {
+ "file-type": "16.5.4",
+ "raw-body": "2.5.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@midwayjs/validate": {
"version": "3.20.13",
"resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.13.tgz",
@@ -377,7 +1232,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -391,7 +1245,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -401,7 +1254,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -465,6 +1317,12 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
"license": "MIT"
},
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "license": "MIT"
+ },
"node_modules/@types/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz",
@@ -675,6 +1533,33 @@
"@types/superagent": "*"
}
},
+ "node_modules/@woocommerce/woocommerce-rest-api": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@woocommerce/woocommerce-rest-api/-/woocommerce-rest-api-1.0.2.tgz",
+ "integrity": "sha512-G+0VwM0MINF83KnT7Rg/htm9EEYADWvDPT/UWEJdZ0de1vXvsPrr4M1ksKaxgKHO8qIJViRrIHCtrui2JoVA+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.8",
+ "create-hmac": "^1.1.7",
+ "oauth-1.0a": "^2.2.6",
+ "url-parse": "^1.4.7"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@@ -688,6 +1573,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -799,9 +1693,9 @@
}
},
"node_modules/axios": {
- "version": "1.12.2",
- "resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
- "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -867,7 +1761,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -982,6 +1875,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
@@ -1017,6 +1923,20 @@
"node": ">=18"
}
},
+ "node_modules/cipher-base": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
+ "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmmirror.com/class-transformer/-/class-transformer-0.5.1.tgz",
@@ -1197,6 +2117,28 @@
"node": ">=8.0.0"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -1215,6 +2157,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1248,12 +2200,20 @@
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1279,7 +2239,6 @@
"version": "2.1.4",
"resolved": "https://registry.npmmirror.com/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
- "dev": true,
"license": "MIT"
},
"node_modules/cookies": {
@@ -1301,6 +2260,51 @@
"integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==",
"license": "MIT"
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
"node_modules/cron": {
"version": "3.5.0",
"resolved": "https://registry.npmmirror.com/cron/-/cron-3.5.0.tgz",
@@ -1343,6 +2347,12 @@
"node": ">= 8"
}
},
+ "node_modules/csv-parse": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz",
+ "integrity": "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==",
+ "license": "MIT"
+ },
"node_modules/dayjs": {
"version": "1.11.18",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
@@ -1436,6 +2446,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "license": "MIT"
+ },
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
@@ -1446,6 +2462,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/dezalgo/-/dezalgo-1.0.4.tgz",
@@ -1457,6 +2482,12 @@
"wrappy": "1"
}
},
+ "node_modules/diacritics": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
+ "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==",
+ "license": "MIT"
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1502,6 +2533,12 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
+ "node_modules/ebec": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ebec/-/ebec-2.3.0.tgz",
+ "integrity": "sha512-bt+0tSL7223VU3PSVi0vtNLZ8pO1AfWolcPPMk2a/a5H+o/ZU9ky0n3A0zhrR4qzJTN61uPsGIO4ShhOukdzxA==",
+ "license": "MIT"
+ },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1532,6 +2569,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/envix": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/envix/-/envix-1.5.0.tgz",
+ "integrity": "sha512-IOxTKT+tffjxgvX2O5nq6enbkv6kBQ/QdMy18bZWo0P0rKPvsRp2/EypIPwTvJfnmk3VdOlq/KcRSZCswefM/w==",
+ "license": "MIT",
+ "dependencies": {
+ "std-env": "^3.7.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1577,6 +2626,48 @@
"node": ">= 0.4"
}
},
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
@@ -1592,11 +2683,40 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
+ "node_modules/eta": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/eta/-/eta-4.4.1.tgz",
+ "integrity": "sha512-4o6fYxhRmFmO9SJcU9PxBLYPGapvJ/Qha0ZE+Y6UE9QIUd0Wk1qaLISQ6J1bM7nOcWHhs1YmY3mfrfwkJRBTWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/bgub/eta?sponsor=1"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -1620,17 +2740,32 @@
"version": "1.19.1",
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
- "dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
+ "node_modules/file-type": {
+ "version": "16.5.4",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
+ "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-web-to-node-stream": "^3.0.0",
+ "strtok3": "^6.2.4",
+ "token-types": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -1639,6 +2774,15 @@
"node": ">=8"
}
},
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -1722,6 +2866,15 @@
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
@@ -1773,6 +2926,18 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1847,7 +3012,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -1928,6 +3092,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hash-base": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
+ "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
@@ -2003,6 +3182,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/i18n-iso-countries": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz",
+ "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==",
+ "license": "MIT",
+ "dependencies": {
+ "diacritics": "1.3.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz",
@@ -2064,6 +3255,12 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -2093,7 +3290,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -2130,7 +3326,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -2143,7 +3338,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -2215,6 +3409,15 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmmirror.com/joi/-/joi-17.13.3.tgz",
@@ -2397,6 +3600,29 @@
"node": ">= 0.6"
}
},
+ "node_modules/li": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/li/-/li-1.3.0.tgz",
+ "integrity": "sha512-z34TU6GlMram52Tss5mt1m//ifRIpKH5Dqm7yUVOdHI+BQCs9qGPHFaCUTIzsWX7edN30aa2WrPwR7IO10FHaw==",
+ "license": "MIT"
+ },
+ "node_modules/locter": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/locter/-/locter-2.2.1.tgz",
+ "integrity": "sha512-Cc7mowptFl7ug5he6Iuos7aGRd9xbwTfnx1ng4AX/7F4iqemPaXAIJDi13IBwQZrKgli9OPEYXm6uCKr7ynxUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "destr": "^2.0.5",
+ "ebec": "^2.3.0",
+ "fast-glob": "^3.3.3",
+ "flat": "^5.0.2",
+ "jiti": "^2.6.1",
+ "yaml": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -2445,6 +3671,15 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
+ "node_modules/lower-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+ "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -2487,6 +3722,17 @@
"node": ">= 0.4"
}
},
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
@@ -2500,7 +3746,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -2519,7 +3764,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -2533,7 +3777,6 @@
"version": "2.6.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
- "dev": true,
"license": "MIT",
"bin": {
"mime": "cli.js"
@@ -2659,9 +3902,9 @@
}
},
"node_modules/mysql2": {
- "version": "3.15.0",
- "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.15.0.tgz",
- "integrity": "sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==",
+ "version": "3.15.3",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
+ "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
@@ -2699,6 +3942,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/no-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+ "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^2.0.2",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/nodemailer": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.6.tgz",
@@ -2718,6 +3971,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npm-check-updates": {
+ "version": "19.1.2",
+ "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.1.2.tgz",
+ "integrity": "sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==",
+ "license": "Apache-2.0",
+ "bin": {
+ "ncu": "build/cli.js",
+ "npm-check-updates": "build/cli.js"
+ },
+ "engines": {
+ "node": ">=20.0.0",
+ "npm": ">=8.12.1"
+ }
+ },
+ "node_modules/oauth-1.0a": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz",
+ "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==",
+ "license": "MIT"
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -2763,6 +4036,15 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
+ "node_modules/parse-link-header": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-1.0.1.tgz",
+ "integrity": "sha512-Z0gpfHmwCIKDr5rRzjypL+p93aHVWO7e+0rFcUl9E3sC67njjs+xHFenuboSXZGlvYtmQqRzRaE3iFpTUnLmFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "~4.0.1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
@@ -2772,6 +4054,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/pascal-case": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
+ "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
@@ -2819,6 +4111,19 @@
"node": ">=8"
}
},
+ "node_modules/peek-readable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
+ "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
@@ -2853,6 +4158,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -2861,7 +4181,7 @@
},
"node_modules/qs": {
"version": "6.14.0",
- "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
@@ -2874,6 +4194,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "license": "MIT"
+ },
"node_modules/queue-lit": {
"version": "1.5.2",
"resolved": "https://registry.npmmirror.com/queue-lit/-/queue-lit-1.5.2.tgz",
@@ -2888,7 +4214,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2905,6 +4230,25 @@
],
"license": "MIT"
},
+ "node_modules/rapiq": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/rapiq/-/rapiq-0.9.0.tgz",
+ "integrity": "sha512-k4oT4RarFBrlLMJ49xUTeQpa/us0uU4I70D/UEnK3FWQ4GENzei01rEQAmvPKAIzACo4NMW+YcYJ7EVfSa7EFg==",
+ "license": "MIT",
+ "dependencies": {
+ "ebec": "^1.1.0",
+ "smob": "^1.4.0"
+ }
+ },
+ "node_modules/rapiq/node_modules/ebec": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ebec/-/ebec-1.1.1.tgz",
+ "integrity": "sha512-JZ1vcvPQtR+8LGbZmbjG21IxLQq/v47iheJqn2F6yB2CgnGfn8ZVg3myHrf3buIZS8UCwQK0jOSIb3oHX7aH8g==",
+ "license": "MIT",
+ "dependencies": {
+ "smob": "^1.4.0"
+ }
+ },
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
@@ -2932,6 +4276,74 @@
"node": ">=0.10.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readable-stream/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/readable-web-to-node-stream": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
+ "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^4.7.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/readable-web-to-node-stream/node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/readable-web-to-node-stream/node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@@ -2960,6 +4372,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "license": "MIT"
+ },
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -2974,18 +4392,29 @@
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
+ "node_modules/ripemd160": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
+ "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.1.2",
+ "inherits": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -3129,6 +4558,45 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sharp": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3240,6 +4708,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
@@ -3250,6 +4727,12 @@
"node": ">=8"
}
},
+ "node_modules/smob": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
+ "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
+ "license": "MIT"
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@@ -3296,6 +4779,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
@@ -3305,6 +4800,27 @@
"node": ">= 0.8"
}
},
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "license": "MIT"
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
@@ -3401,6 +4917,23 @@
"node": ">=8"
}
},
+ "node_modules/strtok3": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
+ "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "peek-readable": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/superagent": {
"version": "8.1.2",
"resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
@@ -3467,9 +5000,9 @@
}
},
"node_modules/to-buffer": {
- "version": "1.2.1",
- "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.1.tgz",
- "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
+ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
"license": "MIT",
"dependencies": {
"isarray": "^2.0.5",
@@ -3484,7 +5017,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -3502,6 +5034,23 @@
"node": ">=0.6"
}
},
+ "node_modules/token-types": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
+ "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/tsc-alias": {
"version": "1.8.16",
"resolved": "https://registry.npmmirror.com/tsc-alias/-/tsc-alias-1.8.16.tgz",
@@ -3539,6 +5088,26 @@
"node": ">=0.6.x"
}
},
+ "node_modules/tsx": {
+ "version": "4.20.6",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
+ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.25.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
@@ -3668,6 +5237,113 @@
}
}
},
+ "node_modules/typeorm-extension": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/typeorm-extension/-/typeorm-extension-3.7.2.tgz",
+ "integrity": "sha512-OGxx9RYqxohyfZnfr8JEGEcO3KCNDmh+fSbgxrNH2/pBN7Deprv5M+BmKIFjbNRi0mUHW4d0yx7ALoDUxU87Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "consola": "^3.4.0",
+ "envix": "^1.5.0",
+ "locter": "^2.2.1",
+ "pascal-case": "^3.1.2",
+ "rapiq": "^0.9.0",
+ "reflect-metadata": "^0.2.2",
+ "smob": "^1.5.0",
+ "yargs": "^18.0.0"
+ },
+ "bin": {
+ "typeorm-extension": "bin/cli.cjs",
+ "typeorm-extension-esm": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@faker-js/faker": ">=8.4.1",
+ "typeorm": "~0.3.0"
+ }
+ },
+ "node_modules/typeorm-extension/node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/typeorm-extension/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "license": "MIT"
+ },
+ "node_modules/typeorm-extension/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typeorm-extension/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/typeorm-extension/node_modules/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/typeorm-extension/node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -3697,6 +5373,22 @@
"node": ">= 0.8"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
@@ -3755,6 +5447,98 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/wpapi": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/wpapi/-/wpapi-1.2.2.tgz",
+ "integrity": "sha512-lkgi8Gjav3SArrCkNpG61ZnmCyamXKB+SjaR8tAoHhSZbJRTeabIlsdqUUAN3JGbVY3ht8p+EGdpCFIaanI5+w==",
+ "license": "MIT",
+ "dependencies": {
+ "li": "^1.3.0",
+ "parse-link-header": "^1.0.1",
+ "qs": "^6.6.0",
+ "superagent": "^4.0.0"
+ }
+ },
+ "node_modules/wpapi/node_modules/form-data": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
+ "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.35",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/wpapi/node_modules/formidable": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
+ "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
+ "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
+ "license": "MIT",
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "node_modules/wpapi/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/wpapi/node_modules/superagent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-4.1.0.tgz",
+ "integrity": "sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==",
+ "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net",
+ "license": "MIT",
+ "dependencies": {
+ "component-emitter": "^1.2.0",
+ "cookiejar": "^2.1.2",
+ "debug": "^4.1.0",
+ "form-data": "^2.3.3",
+ "formidable": "^1.2.0",
+ "methods": "^1.1.1",
+ "mime": "^2.4.0",
+ "qs": "^6.6.0",
+ "readable-stream": "^3.0.6"
+ },
+ "engines": {
+ "node": ">= 6.0"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -3853,6 +5637,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz",
@@ -3875,6 +5680,15 @@
"node": ">=4.0"
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
@@ -3894,6 +5708,18 @@
"node": ">=18"
}
},
+ "node_modules/yaml": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
diff --git a/package.json b/package.json
index d8b9c90..17bde91 100644
--- a/package.json
+++ b/package.json
@@ -15,16 +15,26 @@
"@midwayjs/logger": "^3.1.0",
"@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0",
+ "@midwayjs/upload": "^3.20.16",
"@midwayjs/validate": "^3.20.2",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
- "axios": "^1.7.9",
+ "axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
+ "csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
- "mysql2": "^3.11.5",
+ "eta": "^4.4.1",
+ "i18n-iso-countries": "^7.14.0",
+ "mysql2": "^3.15.3",
"nodemailer": "^7.0.5",
+ "npm-check-updates": "^19.1.2",
+ "qs": "^6.14.0",
+ "sharp": "^0.33.3",
"swagger-ui-dist": "^5.18.2",
- "typeorm": "^0.3.20",
+ "typeorm": "^0.3.27",
+ "typeorm-extension": "^3.7.2",
+ "wpapi": "^1.2.2",
+ "xlsx": "^0.18.5",
"xml2js": "^0.6.2"
},
"engines": {
@@ -36,10 +46,15 @@
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
"test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage",
- "lint": "mwts check",
- "lint:fix": "mwts fix",
+ "lint": "mwtsc check",
+ "lint:fix": "mwtsc fix",
"ci": "npm run cov",
- "build": "mwtsc --cleanOutDir"
+ "build": "mwtsc --cleanOutDir",
+ "seed": "ts-node src/db/seed/index.ts",
+ "seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/db/datasource.ts",
+ "typeorm": "ts-node ./node_modules/typeorm/cli.js",
+ "migration:generate": "npm run typeorm -- -d src/db/datasource.ts migration:generate",
+ "migration:run": "npm run typeorm -- migration:run -d src/db/datasource.ts"
},
"repository": {
"type": "git",
@@ -51,6 +66,7 @@
"@midwayjs/mock": "^3.20.11",
"cross-env": "^10.1.0",
"mwtsc": "^1.15.2",
+ "tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}
diff --git a/permutation_fix.md b/permutation_fix.md
new file mode 100644
index 0000000..de227d0
--- /dev/null
+++ b/permutation_fix.md
@@ -0,0 +1,184 @@
+# Permutation页面列表显示问题分析和修复方案
+
+## 问题分析
+
+经过代码分析,发现了以下几个可能导致列表不显示的问题:
+
+### 1. API路径不匹配
+前端代码中引用的API函数名与后端控制器中的路径不一致:
+- 前端:`productcontrollerGetcategoriesall`、`productcontrollerGetcategoryattributes`、`productcontrollerGetproductlist`
+- 后端实际的API路径:`/product/categories/all`、`/product/category/:id/attributes`、`/product/list`
+
+### 2. 数据格式问题
+- `getCategoryAttributes`返回的数据结构与前端期望的不匹配
+- 属性值获取逻辑可能存在问题
+
+### 3. 组合生成逻辑问题
+- 在生成排列组合时,数据结构和键值对应可能不正确
+
+## 修复方案
+
+### 后端修复
+
+1. **修改getCategoryAttributes方法** - 在`/Users/zksu/Developer/work/workcode/API/src/service/product.service.ts`中:
+
+```typescript
+// 获取分类下的属性配置
+async getCategoryAttributes(categoryId: number): Promise {
+ const category = await this.categoryModel.findOne({
+ where: { id: categoryId },
+ relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'],
+ });
+
+ if (!category) {
+ return [];
+ }
+
+ // 格式化返回,匹配前端期望的数据结构
+ return category.attributes.map(attr => ({
+ id: attr.id,
+ dictId: attr.attributeDict.id,
+ name: attr.attributeDict.name, // 用于generateKeyFromPermutation
+ title: attr.attributeDict.title, // 用于列标题
+ dict: {
+ id: attr.attributeDict.id,
+ name: attr.attributeDict.name,
+ title: attr.attributeDict.title,
+ items: attr.attributeDict.items || []
+ }
+ }));
+}
+```
+
+2. **确保dict/items接口可用** - 检查字典项获取接口:
+
+在`/Users/zksu/Developer/work/workcode/API/src/controller/dict.controller.ts`中添加或确认:
+
+```typescript
+@Get('/items')
+async getDictItems(@Query('dictId') dictId: number) {
+ try {
+ const dict = await this.dictModel.findOne({
+ where: { id: dictId },
+ relations: ['items']
+ });
+
+ if (!dict) {
+ return [];
+ }
+
+ return dict.items || [];
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+}
+```
+
+### 前端修复建议
+
+1. **添加错误处理和调试信息**:
+
+```typescript
+// 在获取属性值的地方添加错误处理
+const fetchData = async () => {
+ setLoading(true);
+ try {
+ // 1. Fetch Attributes
+ const attrRes = await productcontrollerGetcategoryattributes({
+ id: categoryId,
+ });
+ console.log('Attributes response:', attrRes); // 调试用
+ const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
+ setAttributes(attrs);
+
+ // 2. Fetch Attribute Values (Dict Items)
+ const valuesMap: Record = {};
+ for (const attr of attrs) {
+ const dictId = attr.dict?.id || attr.dictId;
+ if (dictId) {
+ try {
+ const itemsRes = await request('/dict/items', {
+ params: { dictId },
+ });
+ console.log(`Dict items for ${attr.name}:`, itemsRes); // 调试用
+ valuesMap[attr.name] = itemsRes || [];
+ } catch (error) {
+ console.error(`Failed to fetch items for dict ${dictId}:`, error);
+ valuesMap[attr.name] = [];
+ }
+ }
+ }
+ setAttributeValues(valuesMap);
+
+ // 3. Fetch Existing Products
+ await fetchProducts(categoryId);
+ } catch (error) {
+ console.error('Error in fetchData:', error);
+ message.error('获取数据失败');
+ } finally {
+ setLoading(false);
+ }
+};
+```
+
+2. **修复组合生成逻辑**:
+
+```typescript
+// 修改generateKeyFromPermutation函数
+const generateKeyFromPermutation = (perm: any) => {
+ const parts = Object.keys(perm).map((attrName) => {
+ const valItem = perm[attrName];
+ const val = valItem.name || valItem.value; // 兼容不同的数据格式
+ return `${attrName}:${val}`;
+ });
+ return parts.sort().join('|');
+};
+
+// 修改generateAttributeKey函数
+const generateAttributeKey = (attrs: any[]) => {
+ const parts = attrs.map((a) => {
+ const key = a.dict?.name || a.dictName || a.name;
+ const val = a.name || a.value;
+ return `${key}:${val}`;
+ });
+ return parts.sort().join('|');
+};
+```
+
+3. **添加空状态处理**:
+
+```typescript
+// 在ProTable中添加空状态提示
+
+```
+
+## 调试步骤
+
+1. **检查网络请求**:
+ - 打开浏览器开发者工具
+ - 检查 `/product/categories/all` 请求是否成功
+ - 检查 `/product/category/:id/attributes` 请求返回的数据格式
+ - 检查 `/dict/items?dictId=:id` 请求是否成功
+ - 检查 `/product/list` 请求是否成功
+
+2. **检查控制台日志**:
+ - 查看属性数据是否正确加载
+ - 查看属性值是否正确获取
+ - 查看排列组合是否正确生成
+
+3. **检查数据结构**:
+ - 确认 `attributes` 数组是否正确
+ - 确认 `attributeValues` 对象是否正确填充
+ - 确认 `permutations` 数组是否正确生成
+
+## 测试验证
+
+1. 选择一个有属性配置的分类
+2. 确认属性有对应的字典项
+3. 检查排列组合是否正确显示
+4. 验证现有产品匹配是否正确
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c7fd255..ff03e1f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -65,12 +65,18 @@ importers:
nodemailer:
specifier: ^7.0.5
version: 7.0.10
+ npm-check-updates:
+ specifier: ^19.1.2
+ version: 19.1.2
swagger-ui-dist:
specifier: ^5.18.2
version: 5.30.2
typeorm:
specifier: ^0.3.20
version: 0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)
+ xlsx:
+ specifier: ^0.18.5
+ version: 0.18.5
xml2js:
specifier: ^0.6.2
version: 0.6.2
@@ -84,6 +90,9 @@ importers:
mwtsc:
specifier: ^1.15.2
version: 1.15.2
+ tsx:
+ specifier: ^4.20.6
+ version: 4.20.6
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -97,6 +106,162 @@ packages:
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
'@hapi/bourne@3.0.0':
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
@@ -314,6 +479,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
+ adler-32@1.3.1:
+ resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
+ engines: {node: '>=0.8'}
+
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -415,6 +584,10 @@ packages:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
+ cfb@1.2.2:
+ resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
+ engines: {node: '>=0.8'}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -446,6 +619,10 @@ packages:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
+ codepage@1.15.0:
+ resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
+ engines: {node: '>=0.8'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -488,6 +665,11 @@ packages:
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+ crc-32@1.2.2:
+ resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
+ engines: {node: '>=0.8'}
+ hasBin: true
+
create-hash@1.2.0:
resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==}
@@ -606,6 +788,11 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -651,6 +838,10 @@ packages:
formidable@2.1.5:
resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==}
+ frac@1.1.2:
+ resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
+ engines: {node: '>=0.8'}
+
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@@ -961,6 +1152,11 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
+ npm-check-updates@19.1.2:
+ resolution: {integrity: sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==}
+ engines: {node: '>=20.0.0', npm: '>=8.12.1'}
+ hasBin: true
+
oauth-1.0a@2.2.6:
resolution: {integrity: sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==}
@@ -1160,6 +1356,10 @@ packages:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
+ ssf@0.11.2:
+ resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
+ engines: {node: '>=0.8'}
+
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
@@ -1228,6 +1428,11 @@ packages:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
+ tsx@4.20.6:
+ resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -1327,6 +1532,14 @@ packages:
engines: {node: '>= 8'}
hasBin: true
+ wmf@1.0.2:
+ resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
+ engines: {node: '>=0.8'}
+
+ word@0.3.0:
+ resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
+ engines: {node: '>=0.8'}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -1338,6 +1551,11 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ xlsx@0.18.5:
+ resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
+ engines: {node: '>=0.8'}
+ hasBin: true
+
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
@@ -1373,6 +1591,84 @@ snapshots:
'@epic-web/invariant@1.0.0': {}
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
'@hapi/bourne@3.0.0': {}
'@hapi/hoek@9.3.0': {}
@@ -1645,6 +1941,8 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
+ adler-32@1.3.1: {}
+
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
@@ -1735,6 +2033,11 @@ snapshots:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
+ cfb@1.2.2:
+ dependencies:
+ adler-32: 1.3.1
+ crc-32: 1.2.2
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -1779,6 +2082,8 @@ snapshots:
co@4.6.0: {}
+ codepage@1.15.0: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -1812,6 +2117,8 @@ snapshots:
core-util-is@1.0.3: {}
+ crc-32@1.2.2: {}
+
create-hash@1.2.0:
dependencies:
cipher-base: 1.0.7
@@ -1919,6 +2226,35 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -1967,6 +2303,8 @@ snapshots:
once: 1.4.0
qs: 6.14.0
+ frac@1.1.2: {}
+
fresh@0.5.2: {}
fsevents@2.3.3:
@@ -2313,6 +2651,8 @@ snapshots:
normalize-path@3.0.0: {}
+ npm-check-updates@19.1.2: {}
+
oauth-1.0a@2.2.6: {}
object-inspect@1.13.4: {}
@@ -2494,6 +2834,10 @@ snapshots:
sqlstring@2.3.3: {}
+ ssf@0.11.2:
+ dependencies:
+ frac: 1.1.2
+
statuses@1.5.0: {}
statuses@2.0.1: {}
@@ -2582,6 +2926,13 @@ snapshots:
tsscmp@1.0.6: {}
+ tsx@4.20.6:
+ dependencies:
+ esbuild: 0.25.12
+ get-tsconfig: 4.13.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@@ -2647,6 +2998,10 @@ snapshots:
dependencies:
isexe: 2.0.0
+ wmf@1.0.2: {}
+
+ word@0.3.0: {}
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -2661,6 +3016,16 @@ snapshots:
wrappy@1.0.2: {}
+ xlsx@0.18.5:
+ dependencies:
+ adler-32: 1.3.1
+ cfb: 1.2.2
+ codepage: 1.15.0
+ crc-32: 1.2.2
+ ssf: 0.11.2
+ wmf: 1.0.2
+ word: 0.3.0
+
xml2js@0.6.2:
dependencies:
sax: 1.4.3
diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts
new file mode 100644
index 0000000..b210e4a
--- /dev/null
+++ b/src/adapter/shopyy.adapter.ts
@@ -0,0 +1,678 @@
+import { ISiteAdapter } from '../interface/site-adapter.interface';
+import { ShopyyService } from '../service/shopyy.service';
+import {
+ UnifiedAddressDTO,
+ UnifiedCustomerDTO,
+ UnifiedMediaDTO,
+ UnifiedOrderDTO,
+ UnifiedOrderLineItemDTO,
+ UnifiedPaginationDTO,
+ UnifiedProductDTO,
+ UnifiedProductVariationDTO,
+ UnifiedSearchParamsDTO,
+ UnifiedSubscriptionDTO,
+ UnifiedReviewPaginationDTO,
+ UnifiedReviewDTO,
+ UnifiedWebhookDTO,
+ UnifiedWebhookPaginationDTO,
+ CreateWebhookDTO,
+ UpdateWebhookDTO,
+} from '../dto/site-api.dto';
+import {
+ ShopyyCustomer,
+ ShopyyOrder,
+ ShopyyProduct,
+ ShopyyVariant,
+ ShopyyWebhook,
+} from '../dto/shopyy.dto';
+
+export class ShopyyAdapter implements ISiteAdapter {
+ constructor(private site: any, private shopyyService: ShopyyService) {
+ this.mapCustomer = this.mapCustomer.bind(this);
+ this.mapProduct = this.mapProduct.bind(this);
+ this.mapVariation = this.mapVariation.bind(this);
+ this.mapOrder = this.mapOrder.bind(this);
+ this.mapMedia = this.mapMedia.bind(this);
+ // this.mapSubscription = this.mapSubscription.bind(this);
+ }
+
+ private mapMedia(item: any): UnifiedMediaDTO {
+ // 映射媒体项目
+ return {
+ id: item.id,
+ date_created: item.created_at,
+ date_modified: item.updated_at,
+ source_url: item.src,
+ title: item.alt || '',
+ media_type: '', // Shopyy API未提供,暂时留空
+ mime_type: '', // Shopyy API未提供,暂时留空
+ };
+ }
+
+ private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any {
+ const { search, page, per_page } = params;
+ const shopyyParams: any = {
+ page: page || 1,
+ limit: per_page || 10,
+ };
+
+ if (search) {
+ shopyyParams.query = search;
+ }
+
+ return shopyyParams;
+ }
+
+ private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO {
+ // 映射产品状态
+ function mapProductStatus(status: number) {
+ return status === 1 ? 'publish' : 'draft';
+ }
+ return {
+ id: item.id,
+ name: item.name || item.title,
+ type: String(item.product_type ?? ''),
+ status: mapProductStatus(item.status),
+ sku: item.variant?.sku || '',
+ 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) => ({
+ id: img.id || 0,
+ src: img.src,
+ name: '',
+ alt: img.alt || '',
+ // 排序
+ position: img.position || '',
+ })),
+ attributes: (item.options || []).map(option => ({
+ id: option.id || 0,
+ name: option.option_name || '',
+ options: (option.values || []).map(value => value.option_value || ''),
+ })),
+ tags: (item.tags || []).map((t: any) => ({
+ id: t.id || 0,
+ name: t.name || '',
+ })),
+ // shopyy叫做专辑
+ categories: item.collections.map((c: any) => ({
+ id: c.id || 0,
+ name: c.title || '',
+ })),
+ variations: item.variants?.map(this.mapVariation.bind(this)) || [],
+ permalink: item.permalink,
+ 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,
+ };
+ }
+
+ private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO {
+ // 映射变体
+ return {
+ id: variant.id,
+ sku: variant.sku || '',
+ regular_price: String(variant.price ?? ''),
+ sale_price: String(variant.special_price ?? ''),
+ price: String(variant.price ?? ''),
+ stock_status:
+ variant.inventory_tracking === 1 ? 'instock' : 'outofstock',
+ stock_quantity: variant.inventory_quantity,
+ };
+ }
+
+ private mapOrder(item: ShopyyOrder): UnifiedOrderDTO {
+ // 提取账单和送货地址 如果不存在则为空对象
+ const billing = (item as any).billing_address || {};
+ const shipping = (item as any).shipping_address || {};
+
+ // 构建账单地址对象
+ const billingObj: UnifiedAddressDTO = {
+ first_name: billing.first_name || item.firstname || '',
+ last_name: billing.last_name || item.lastname || '',
+ fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
+ company: billing.company || '',
+ email: item.customer_email || item.email || '',
+ phone: billing.phone || (item as any).telephone || '',
+ address_1: billing.address1 || item.payment_address || '',
+ address_2: billing.address2 || '',
+ city: billing.city || item.payment_city || '',
+ state: billing.province || item.payment_zone || '',
+ postcode: billing.zip || item.payment_postcode || '',
+ country:
+ billing.country_name ||
+ billing.country_code ||
+ item.payment_country ||
+ '',
+ };
+
+ // 构建送货地址对象
+ const shippingObj: UnifiedAddressDTO = {
+ first_name: shipping.first_name || item.firstname || '',
+ last_name: shipping.last_name || item.lastname || '',
+ fullname: shipping.name || '',
+ company: shipping.company || '',
+ 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 || '',
+ postcode: shipping.zip || item.shipping_postcode || '',
+ country:
+ shipping.country_name ||
+ shipping.country_code ||
+ item.shipping_country ||
+ '',
+ };
+
+ // 格式化地址为字符串
+ const formatAddress = (addr: UnifiedAddressDTO) => {
+ return [
+ addr.fullname,
+ addr.company,
+ addr.address_1,
+ addr.address_2,
+ addr.city,
+ addr.state,
+ addr.postcode,
+ addr.country,
+ addr.phone,
+ ]
+ .filter(Boolean)
+ .join(', ');
+ };
+
+ const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
+ (p: any) => ({
+ id: p.id,
+ name: p.product_title || p.name,
+ product_id: p.product_id,
+ quantity: p.quantity,
+ total: String(p.price ?? ''),
+ sku: p.sku || p.sku_code || '',
+ })
+ );
+
+ return {
+ id: item.id || item.order_id,
+ 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 ?? ''),
+ customer_id: item.customer_id || item.user_id,
+ customer_name:
+ item.customer_name || `${item.firstname} ${item.lastname}`.trim(),
+ email: item.customer_email || item.email,
+ line_items: lineItems,
+ sales: lineItems, // 兼容前端
+ billing: billingObj,
+ shipping: shippingObj,
+ billing_full_address: formatAddress(billingObj),
+ shipping_full_address: formatAddress(shippingObj),
+ payment_method: item.payment_method,
+ refunds: [],
+ 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: ShopyyCustomer): UnifiedCustomerDTO {
+ // 处理多地址结构
+ const addresses = item.addresses || [];
+ const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {});
+
+ // 尝试从地址列表中获取billing和shipping
+ // 如果没有明确区分,默认使用默认地址或第一个地址
+ const billingAddress = defaultAddress;
+ const shippingAddress = defaultAddress;
+
+ const billing = {
+ first_name: billingAddress.first_name || item.first_name || '',
+ last_name: billingAddress.last_name || item.last_name || '',
+ fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(),
+ company: billingAddress.company || '',
+ email: item.email || '',
+ phone: billingAddress.phone || item.contact || '',
+ address_1: billingAddress.address1 || '',
+ address_2: billingAddress.address2 || '',
+ city: billingAddress.city || '',
+ state: billingAddress.province || '',
+ postcode: billingAddress.zip || '',
+ country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || ''
+ };
+
+ const shipping = {
+ first_name: shippingAddress.first_name || item.first_name || '',
+ last_name: shippingAddress.last_name || item.last_name || '',
+ fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(),
+ company: shippingAddress.company || '',
+ address_1: shippingAddress.address1 || '',
+ address_2: shippingAddress.address2 || '',
+ city: shippingAddress.city || '',
+ state: shippingAddress.province || '',
+ postcode: shippingAddress.zip || '',
+ country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || ''
+ };
+
+ 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(),
+ email: item.email || item.customer_email || '',
+ phone: item.contact || billing.phone || item.phone || '',
+ billing,
+ shipping,
+ 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,
+ };
+ }
+
+ async getProducts(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ const response = await this.shopyyService.fetchResourcePaged(
+ this.site,
+ 'products/list',
+ params
+ );
+ const { items=[], total, totalPages, page, per_page } = response;
+ const finalItems = items.map((item) => ({
+ ...item,
+ permalink: `${this.site.websiteUrl}/products/${item.handle}`,
+ })).map(this.mapProduct.bind(this))
+ return {
+ items: finalItems as UnifiedProductDTO[],
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ async getProduct(id: string | number): Promise {
+ // 使用ShopyyService获取单个产品
+ const product = await this.shopyyService.getProduct(this.site, id);
+ return this.mapProduct(product);
+ }
+
+ async createProduct(data: Partial): Promise {
+ const res = await this.shopyyService.createProduct(this.site, data);
+ return this.mapProduct(res);
+ }
+
+ async updateProduct(id: string | number, data: Partial): Promise {
+ // Shopyy update returns boolean?
+ // shopyyService.updateProduct returns boolean.
+ // So I can't return the updated product.
+ // I have to fetch it again or return empty/input.
+ // Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock).
+ await this.shopyyService.updateProduct(this.site, String(id), data);
+ return true;
+ }
+
+ async updateVariation(productId: string | number, variationId: string | number, data: any): Promise {
+ await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data);
+ return { ...data, id: variationId };
+ }
+
+ async getOrderNotes(orderId: string | number): Promise {
+ return await this.shopyyService.getOrderNotes(this.site, orderId);
+ }
+
+ async createOrderNote(orderId: string | number, data: any): Promise {
+ return await this.shopyyService.createOrderNote(this.site, orderId, data);
+ }
+
+ async deleteProduct(id: string | number): Promise {
+ // Use batch delete
+ await this.shopyyService.batchProcessProducts(this.site, { delete: [id] });
+ return true;
+ }
+
+ async batchProcessProducts(
+ data: { create?: any[]; update?: any[]; delete?: Array }
+ ): Promise {
+ return await this.shopyyService.batchProcessProducts(this.site, data);
+ }
+
+ async getOrders(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ const { items, total, totalPages, page, per_page } =
+ await this.shopyyService.fetchResourcePaged(
+ this.site,
+ 'orders',
+ params
+ );
+ return {
+ items: items.map(this.mapOrder.bind(this)),
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ async getOrder(id: string | number): Promise {
+ const data = await this.shopyyService.getOrder(String(this.site.id), String(id));
+ return this.mapOrder(data);
+ }
+
+ async createOrder(data: Partial): Promise {
+ const createdOrder = await this.shopyyService.createOrder(this.site, data);
+ return this.mapOrder(createdOrder);
+ }
+
+ async updateOrder(id: string | number, data: Partial): Promise {
+ return await this.shopyyService.updateOrder(this.site, String(id), data);
+ }
+
+ async deleteOrder(id: string | number): Promise {
+ return await this.shopyyService.deleteOrder(this.site, id);
+ }
+
+ async shipOrder(orderId: string | number, data: {
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: Array<{
+ order_item_id: number;
+ quantity: number;
+ }>;
+ }): Promise {
+ // 订单发货
+ try {
+ // 更新订单状态为已发货
+ await this.shopyyService.updateOrder(this.site, String(orderId), {
+ status: 'completed',
+ meta_data: [
+ { key: '_tracking_number', value: data.tracking_number },
+ { key: '_shipping_provider', value: data.shipping_provider },
+ { key: '_shipping_method', value: data.shipping_method }
+ ]
+ });
+
+ // 添加发货备注
+ const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`;
+ await this.shopyyService.createOrderNote(this.site, orderId, { note, customer_note: true });
+
+ return {
+ success: true,
+ order_id: orderId,
+ shipment_id: `shipment_${orderId}_${Date.now()}`,
+ tracking_number: data.tracking_number,
+ shipping_provider: data.shipping_provider,
+ shipped_at: new Date().toISOString()
+ };
+ } catch (error) {
+ throw new Error(`发货失败: ${error.message}`);
+ }
+ }
+
+ async cancelShipOrder(orderId: string | number, data: {
+ reason?: string;
+ shipment_id?: string;
+ }): Promise {
+ // 取消订单发货
+ try {
+ // 将订单状态改回处理中
+ await this.shopyyService.updateOrder(this.site, String(orderId), {
+ status: 'processing',
+ meta_data: [
+ { key: '_shipment_cancelled', value: 'yes' },
+ { key: '_shipment_cancelled_reason', value: data.reason }
+ ]
+ });
+
+ // 添加取消发货的备注
+ const note = `订单发货已取消${data.reason ? `,原因:${data.reason}` : ''}`;
+ await this.shopyyService.createOrderNote(this.site, orderId, { note, customer_note: true });
+
+ return {
+ success: true,
+ order_id: orderId,
+ shipment_id: data.shipment_id,
+ reason: data.reason,
+ cancelled_at: new Date().toISOString()
+ };
+ } catch (error) {
+ throw new Error(`取消发货失败: ${error.message}`);
+ }
+ }
+
+ async getSubscriptions(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ throw new Error('Shopyy does not support subscriptions.');
+ }
+
+ async getMedia(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ const requestParams = this.mapMediaSearchParams(params);
+ const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged(
+ this.site,
+ 'media', // Shopyy的媒体API端点可能需要调整
+ requestParams
+ );
+ return {
+ items: items.map(this.mapMedia),
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ async createMedia(file: any): Promise {
+ const createdMedia = await this.shopyyService.createMedia(this.site, file);
+ return this.mapMedia(createdMedia);
+ }
+
+ async updateMedia(id: string | number, data: any): Promise {
+ const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data);
+ return this.mapMedia(updatedMedia);
+ }
+
+ async deleteMedia(id: string | number): Promise {
+ return await this.shopyyService.deleteMedia(this.site, id);
+ }
+
+ async getReviews(
+ params: UnifiedSearchParamsDTO
+ ): Promise {
+ const requestParams = this.mapReviewSearchParams(params);
+ const { items, total, totalPages, page, per_page } = await this.shopyyService.getReviews(
+ this.site,
+ requestParams
+ );
+ return {
+ items: items.map(this.mapReview),
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ async getReview(id: string | number): Promise {
+ const review = await this.shopyyService.getReview(this.site, id);
+ return this.mapReview(review);
+ }
+
+ private mapReview(review: any): UnifiedReviewDTO {
+ // 将ShopYY评论数据映射到统一评论DTO格式
+ return {
+ id: review.id || review.review_id,
+ product_id: review.product_id || review.goods_id,
+ author: review.author_name || review.username || '',
+ email: review.author_email || review.user_email || '',
+ content: review.comment || review.content || '',
+ rating: Number(review.score || review.rating || 0),
+ status: String(review.status || 'approved'),
+ date_created:
+ typeof review.created_at === 'number'
+ ? new Date(review.created_at * 1000).toISOString()
+ : String(review.created_at || review.date_added || '')
+ };
+ }
+
+ private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any {
+ const { search, page, per_page, status } = params;
+ const shopyyParams: any = {
+ page: page || 1,
+ limit: per_page || 10,
+ };
+
+ if (search) {
+ shopyyParams.search = search;
+ }
+
+ if (status) {
+ shopyyParams.status = status;
+ }
+
+ // if (product_id) {
+ // shopyyParams.product_id = product_id;
+ // }
+
+ return shopyyParams;
+ }
+
+ async createReview(data: any): Promise {
+ const createdReview = await this.shopyyService.createReview(this.site, data);
+ return this.mapReview(createdReview);
+ }
+
+ async updateReview(id: string | number, data: any): Promise {
+ const updatedReview = await this.shopyyService.updateReview(this.site, id, data);
+ return this.mapReview(updatedReview);
+ }
+
+ async deleteReview(id: string | number): Promise {
+ return await this.shopyyService.deleteReview(this.site, id);
+ }
+
+ // Webhook相关方法
+ private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO {
+ return {
+ id: item.id,
+ name: item.webhook_name || `Webhook-${item.id}`,
+ topic: item.event_code || '',
+ delivery_url: item.url|| '',
+ status: 'active',
+ };
+ }
+
+ async getWebhooks(params: UnifiedSearchParamsDTO): Promise {
+ const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params);
+ return {
+ items: items.map(this.mapWebhook),
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ async getWebhook(id: string | number): Promise {
+ const webhook = await this.shopyyService.getWebhook(this.site, id);
+ return this.mapWebhook(webhook);
+ }
+
+ async createWebhook(data: CreateWebhookDTO): Promise {
+ const createdWebhook = await this.shopyyService.createWebhook(this.site, data);
+ return this.mapWebhook(createdWebhook);
+ }
+
+ async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise {
+ const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data);
+ return this.mapWebhook(updatedWebhook);
+ }
+
+ async deleteWebhook(id: string | number): Promise {
+ return await this.shopyyService.deleteWebhook(this.site, id);
+ }
+
+ async getLinks(): Promise> {
+ // ShopYY站点的管理后台链接通常基于apiUrl构建
+ const url = this.site.websiteUrl
+ // 提取基础域名,去掉可能的路径部分
+ const baseUrl = url.replace(/\/api\/.*$/i, '');
+
+ const links = [
+ { title: '访问网站', url: baseUrl },
+ { title: '管理后台', url: `${baseUrl}/admin/` },
+ { title: '订单管理', url: `${baseUrl}/admin/orders.htm` },
+ { title: '产品管理', url: `${baseUrl}/admin/products.htm` },
+ { title: '客户管理', url: `${baseUrl}/admin/customers.htm` },
+ { title: '插件管理', url: `${baseUrl}/admin/apps.htm` },
+ { title: '店铺设置', url: `${baseUrl}/admin/settings.htm` },
+ { title: '营销中心', url: `${baseUrl}/admin/marketing.htm` },
+ ];
+ return links;
+ }
+
+ async getCustomers(params: UnifiedSearchParamsDTO): Promise> {
+ const { items, total, totalPages, page, per_page } =
+ await this.shopyyService.fetchCustomersPaged(this.site, params);
+ return {
+ items: items.map(this.mapCustomer.bind(this)),
+ total,
+ totalPages,
+ page,
+ per_page
+ };
+ }
+
+ async getCustomer(id: string | number): Promise {
+ const customer = await this.shopyyService.getCustomer(this.site, id);
+ return this.mapCustomer(customer);
+ }
+
+ async createCustomer(data: Partial): Promise {
+ const createdCustomer = await this.shopyyService.createCustomer(this.site, data);
+ return this.mapCustomer(createdCustomer);
+ }
+
+ async updateCustomer(id: string | number, data: Partial): Promise {
+ const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data);
+ return this.mapCustomer(updatedCustomer);
+ }
+
+ async deleteCustomer(id: string | number): Promise {
+ return await this.shopyyService.deleteCustomer(this.site, id);
+ }
+}
diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts
new file mode 100644
index 0000000..64e1168
--- /dev/null
+++ b/src/adapter/woocommerce.adapter.ts
@@ -0,0 +1,852 @@
+import { ISiteAdapter } from '../interface/site-adapter.interface';
+import { IPlatformService } from '../interface/platform.interface';
+import {
+ UnifiedMediaDTO,
+ UnifiedOrderDTO,
+ UnifiedPaginationDTO,
+ UnifiedProductDTO,
+ UnifiedSearchParamsDTO,
+ UnifiedSubscriptionDTO,
+ UnifiedCustomerDTO,
+ UnifiedReviewPaginationDTO,
+ UnifiedReviewDTO,
+ UnifiedWebhookDTO,
+ UnifiedWebhookPaginationDTO,
+ CreateWebhookDTO,
+ UpdateWebhookDTO,
+} from '../dto/site-api.dto';
+import {
+ WooProduct,
+ WooOrder,
+ WooSubscription,
+ WpMedia,
+ WooCustomer,
+ WooWebhook,
+ WooOrderSearchParams,
+ WooProductSearchParams,
+} from '../dto/woocommerce.dto';
+import { Site } from '../entity/site.entity';
+
+export class WooCommerceAdapter implements ISiteAdapter {
+ // 构造函数接收站点配置与服务实例
+ constructor(private site: Site, private wpService: IPlatformService) {
+ this.mapProduct = this.mapProduct.bind(this);
+ this.mapReview = this.mapReview.bind(this);
+ this.mapCustomer = this.mapCustomer.bind(this);
+ this.mapMedia = this.mapMedia.bind(this);
+ this.mapOrder = this.mapOrder.bind(this);
+ this.mapWebhook = this.mapWebhook.bind(this);
+ }
+
+ // 映射 WooCommerce webhook 到统一格式
+ private mapWebhook(webhook: WooWebhook): UnifiedWebhookDTO {
+ return {
+ id: webhook.id.toString(),
+ name: webhook.name,
+ status: webhook.status,
+ topic: webhook.topic,
+ delivery_url: webhook.delivery_url,
+ secret: webhook.secret,
+ api_version: webhook.api_version,
+ date_created: webhook.date_created,
+ date_modified: webhook.date_modified,
+ // metadata: webhook.meta_data || [],
+ };
+ }
+
+ // 获取站点的 webhooks 列表
+ async getWebhooks(params: UnifiedSearchParamsDTO): Promise {
+ try {
+ const result = await this.wpService.getWebhooks(this.site, params);
+
+ return {
+ items: (result.items as WooWebhook[]).map(this.mapWebhook),
+ total: result.total,
+ page: Number(params.page || 1),
+ per_page: Number(params.per_page || 20),
+ totalPages: result.totalPages,
+ };
+ } catch (error) {
+ throw new Error(`Failed to get webhooks: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // 获取单个 webhook 详情
+ async getWebhook(id: string | number): Promise {
+ try {
+ const result = await this.wpService.getWebhook(this.site, id);
+ return this.mapWebhook(result as WooWebhook);
+ } catch (error) {
+ throw new Error(`Failed to get webhook: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // 创建新的 webhook
+ async createWebhook(data: CreateWebhookDTO): Promise {
+ try {
+ const params = {
+ name: data.name,
+ status: 'active', // 默认状态为活跃
+ topic: data.topic,
+ delivery_url: data.delivery_url,
+ secret: data.secret,
+ api_version: data.api_version || 'wp/v2',
+ };
+ const result = await this.wpService.createWebhook(this.site, params);
+ return this.mapWebhook(result as WooWebhook);
+ } catch (error) {
+ throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // 更新现有的 webhook
+ async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise {
+ try {
+ const params = {
+ ...(data.name ? { name: data.name } : {}),
+ ...(data.status ? { status: data.status } : {}),
+ ...(data.topic ? { topic: data.topic } : {}),
+ ...(data.delivery_url ? { delivery_url: data.delivery_url } : {}),
+ ...(data.secret ? { secret: data.secret } : {}),
+ ...(data.api_version ? { api_version: data.api_version } : {}),
+ };
+ const result = await this.wpService.updateWebhook(this.site, id, params);
+ return this.mapWebhook(result as WooWebhook);
+ } catch (error) {
+ throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // 删除指定的 webhook
+ async deleteWebhook(id: string | number): Promise {
+ try {
+ await this.wpService.deleteWebhook(this.site, id);
+ return true;
+ } catch (error) {
+ throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async getLinks(): Promise> {
+ const baseUrl = this.site.apiUrl;
+ const links = [
+ { title: '访问网站', url: baseUrl },
+ { title: '管理后台', url: `${baseUrl}/wp-admin/` },
+ { title: '订单管理', url: `${baseUrl}/wp-admin/edit.php?post_type=shop_order` },
+ { title: '产品管理', url: `${baseUrl}/wp-admin/edit.php?post_type=product` },
+ { title: '客户管理', url: `${baseUrl}/wp-admin/users.php` },
+ { title: '插件管理', url: `${baseUrl}/wp-admin/plugins.php` },
+ { title: '主题管理', url: `${baseUrl}/wp-admin/themes.php` },
+ { title: 'WooCommerce设置', url: `${baseUrl}/wp-admin/admin.php?page=wc-settings` },
+ { title: 'WooCommerce报告', url: `${baseUrl}/wp-admin/admin.php?page=wc-reports` },
+ ];
+ return links;
+ }
+
+ createMedia(file: any): Promise {
+ throw new Error('Method not implemented.');
+ }
+ batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise {
+ throw new Error('Method not implemented.');
+ }
+ batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array; }): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+
+
+ private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial {
+ const page = Number(params.page ?? 1);
+ const per_page = Number( params.per_page ?? 20);
+ const 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];
+ let mappedField = field;
+ if (['created_at', 'date_created', 'date'].includes(field)) mappedField = 'date';
+ else if (['name', 'title'].includes(field)) mappedField = 'title';
+ else if (['id', 'ID'].includes(field)) mappedField = 'id';
+ else if (['price', 'regular_price', 'sale_price'].includes(field)) mappedField = 'price';
+ orderby = mappedField;
+ order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
+ }
+ }
+ const mapped: any = {
+ ...(params.search ? { search: params.search } : {}),
+ ...(params.status ? { status: params.status } : {}),
+ ...(orderby ? { orderby } : {}),
+ ...(order ? { order } : {}),
+ page,
+ per_page,
+ };
+
+ const toArray = (value: any): any[] => {
+ if (Array.isArray(value)) return value;
+ if (value === undefined || value === null) return [];
+ return String(value).split(',').map(v => v.trim()).filter(Boolean);
+ };
+
+ if (where.search_fields ?? where.searchFields) mapped.search_fields = toArray(where.search_fields ?? where.searchFields);
+ if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
+ if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before);
+ if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after);
+ if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before);
+ if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt);
+ if (where.exclude ?? where.exclude_ids ?? where.excludedIds) mapped.exclude = toArray(where.exclude ?? where.exclude_ids ?? where.excludedIds);
+ if (where.include ?? where.ids) mapped.include = toArray(where.include ?? where.ids);
+ if (where.offset !== undefined) mapped.offset = Number(where.offset);
+ if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
+ if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
+ if (where.slug) mapped.slug = String(where.slug);
+ if (!mapped.status && (where.status || where.include_status || where.exclude_status || where.includeStatus || where.excludeStatus)) {
+ if (where.include_status ?? where.includeStatus) mapped.include_status = String(where.include_status ?? where.includeStatus);
+ if (where.exclude_status ?? where.excludeStatus) mapped.exclude_status = String(where.exclude_status ?? where.excludeStatus);
+ if (where.status) mapped.status = String(where.status);
+ }
+ if (where.type) mapped.type = String(where.type);
+ if (where.include_types ?? where.includeTypes) mapped.include_types = String(where.include_types ?? where.includeTypes);
+ if (where.exclude_types ?? where.excludeTypes) mapped.exclude_types = String(where.exclude_types ?? where.excludeTypes);
+ if (where.sku) mapped.sku = String(where.sku);
+ if (where.featured ?? where.isFeatured) mapped.featured = Boolean(where.featured ?? where.isFeatured);
+ if (where.category ?? where.categoryId) mapped.category = String(where.category ?? where.categoryId);
+ if (where.tag ?? where.tagId) mapped.tag = String(where.tag ?? where.tagId);
+ if (where.shipping_class ?? where.shippingClass) mapped.shipping_class = String(where.shipping_class ?? where.shippingClass);
+ if (where.attribute ?? where.attributeName) mapped.attribute = String(where.attribute ?? where.attributeName);
+ if (where.attribute_term ?? where.attributeTermId ?? where.attributeTerm) mapped.attribute_term = String(where.attribute_term ?? where.attributeTermId ?? where.attributeTerm);
+ if (where.tax_class ?? where.taxClass) mapped.tax_class = String(where.tax_class ?? where.taxClass);
+ if (where.on_sale ?? where.onSale) mapped.on_sale = Boolean(where.on_sale ?? where.onSale);
+ if (where.min_price ?? where.minPrice) mapped.min_price = String(where.min_price ?? where.minPrice);
+ if (where.max_price ?? where.maxPrice) mapped.max_price = String(where.max_price ?? where.maxPrice);
+ if (where.stock_status ?? where.stockStatus) mapped.stock_status = String(where.stock_status ?? where.stockStatus);
+ if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual);
+ if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable);
+
+ if (params.ids) {
+ const idsArr = String(params.ids).split(',').map(v => v.trim()).filter(Boolean);
+ mapped.include = idsArr;
+ }
+ return mapped;
+ }
+
+ private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial {
+ // 计算分页参数
+ const page = Number(params.page ?? 1);
+ const per_page = Number( params.per_page ?? 20);
+ // 解析排序参数 支持从 order 对象推导
+ const where = params.where && typeof params.where === 'object' ? params.where : {};
+ let orderby: string | undefined = params.orderby;
+ let orderDir: '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];
+ let mappedField = field;
+ if (['created_at', 'date_created', 'date'].includes(field)) mappedField = 'date';
+ else if (['modified', 'date_modified'].includes(field)) mappedField = 'modified';
+ else if (['id', 'ID'].includes(field)) mappedField = 'id';
+ else if (['name', 'title'].includes(field)) mappedField = 'title';
+ else if (['slug'].includes(field)) mappedField = 'slug';
+ else if (['include'].includes(field)) mappedField = 'include';
+ orderby = mappedField;
+ orderDir = String(dir).toLowerCase() === 'asc' ? 'asc' : 'desc';
+ }
+ } else if (!orderDir && typeof params.order === 'string') {
+ orderDir = String(params.order).toLowerCase() === 'asc' ? 'asc' : 'desc';
+ }
+
+ const mapped: any = {
+ ...(params.search ? { search: params.search } : {}),
+ ...(orderby ? { orderby } : {}),
+ ...(orderDir ? { order: orderDir } : {}),
+ page,
+ per_page,
+ };
+
+ const toArray = (value: any): any[] => {
+ if (Array.isArray(value)) return value;
+ if (value === undefined || value === null) return [];
+ return String(value).split(',').map(v => v.trim()).filter(Boolean);
+ };
+
+ const toNumber = (value: any): number | undefined => {
+ if (value === undefined || value === null || value === '') return undefined;
+ const n = Number(value);
+ return Number.isFinite(n) ? n : undefined;
+ };
+
+ // 时间过滤参数
+ if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
+ if (where.before ?? where.date_created_before ?? where.created_before) mapped.before = String(where.before ?? where.date_created_before ?? where.created_before);
+ if (where.modified_after ?? where.date_modified_after) mapped.modified_after = String(where.modified_after ?? where.date_modified_after);
+ if (where.modified_before ?? where.date_modified_before) mapped.modified_before = String(where.modified_before ?? where.date_modified_before);
+ if (where.dates_are_gmt ?? where.datesAreGmt) mapped.dates_are_gmt = Boolean(where.dates_are_gmt ?? where.datesAreGmt);
+
+ // 集合过滤参数
+ if (where.exclude) mapped.exclude = toArray(where.exclude);
+ if (where.include) mapped.include = toArray(where.include);
+ if (params.ids) mapped.include = String(params.ids).split(',').map(v => v.trim()).filter(Boolean);
+ if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
+ if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
+ if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
+
+ // 状态过滤 参数支持数组或逗号分隔字符串
+ const statusSource = params.status ?? where.status;
+ if (statusSource !== undefined) {
+ mapped.status = Array.isArray(statusSource)
+ ? statusSource.map(s => String(s))
+ : String(statusSource).split(',').map(s => s.trim()).filter(Boolean);
+ }
+
+ // 客户与产品过滤
+ const customerVal = params.customer_id ?? where.customer ?? where.customer_id;
+ const productVal = where.product ?? where.product_id;
+ const dpVal = where.dp;
+ if (toNumber(customerVal) !== undefined) mapped.customer = Number(customerVal);
+ if (toNumber(productVal) !== undefined) mapped.product = Number(productVal);
+ if (toNumber(dpVal) !== undefined) mapped.dp = Number(dpVal);
+
+ // 创建来源过滤 支持逗号分隔
+ const createdViaVal = where.created_via;
+ if (createdViaVal !== undefined) mapped.created_via = Array.isArray(createdViaVal)
+ ? createdViaVal.join(',')
+ : String(createdViaVal);
+
+ return mapped;
+ }
+
+ private mapCustomerSearchParams(params: UnifiedSearchParamsDTO): Record {
+ const page = Number(params.page ?? 1);
+ const per_page = Number(params.per_page ?? 20);
+ const where = params.where && typeof params.where === 'object' ? params.where : {};
+ let orderby: string | undefined = params.orderby;
+ let orderDir: '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];
+ let mappedField = field;
+ if (['id', 'ID'].includes(field)) mappedField = 'id';
+ else if (['include'].includes(field)) mappedField = 'include';
+ else if (['name', 'username'].includes(field)) mappedField = 'name';
+ else if (['registered_date', 'date_created', 'registered', 'registeredDate'].includes(field)) mappedField = 'registered_date';
+ orderby = mappedField;
+ orderDir = String(dir).toLowerCase() === 'asc' ? 'asc' : 'desc';
+ }
+ } else if (!orderDir && typeof params.order === 'string') {
+ orderDir = String(params.order).toLowerCase() === 'asc' ? 'asc' : 'desc';
+ }
+
+ const mapped: any = {
+ ...(params.search ? { search: params.search } : {}),
+ ...(orderby ? { orderby } : {}),
+ ...(orderDir ? { order: orderDir } : {}),
+ page,
+ per_page,
+ };
+
+ const toArray = (value: any): any[] => {
+ if (Array.isArray(value)) return value;
+ if (value === undefined || value === null) return [];
+ return String(value).split(',').map(v => v.trim()).filter(Boolean);
+ };
+
+ const toNumber = (value: any): number | undefined => {
+ if (value === undefined || value === null || value === '') return undefined;
+ const n = Number(value);
+ return Number.isFinite(n) ? n : undefined;
+ };
+
+ if (where.exclude) mapped.exclude = toArray(where.exclude);
+ if (where.include) mapped.include = toArray(where.include);
+ if (params.ids) mapped.include = String(params.ids).split(',').map(v => v.trim()).filter(Boolean);
+ if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
+
+ if (where.email) mapped.email = String(where.email);
+ const roleSource = where.role ?? params.status;
+ if (roleSource !== undefined) mapped.role = String(roleSource);
+
+ return mapped;
+ }
+
+ private mapProduct(item: WooProduct): UnifiedProductDTO {
+ // 将 WooCommerce 产品数据映射为统一产品DTO
+ // 保留常用字段与时间信息以便前端统一展示
+ // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties
+ return {
+ id: item.id,
+ 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,
+ stock_status: item.stock_status,
+ stock_quantity: item.stock_quantity,
+ images: (item.images || []).map((img: any) => ({
+ id: img.id,
+ src: img.src,
+ name: img.name,
+ alt: img.alt,
+ })),
+ categories: (item.categories || []).map((c: any) => ({
+ id: c.id,
+ name: c.name,
+ })),
+ tags: (item.tags || []).map((t: any) => ({
+ id: t.id,
+ name: t.name,
+ })),
+ attributes: (item.attributes || []).map(attr => ({
+ id: attr.id,
+ name: attr.name || '',
+ position: attr.position,
+ visible: attr.visible,
+ variation: attr.variation,
+ options: attr.options || []
+ })),
+ variations: item.variations as any,
+ permalink: item.permalink,
+ raw: item,
+ };
+ }
+ private buildFullAddress(addr: any): string {
+ if (!addr) return '';
+ const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim();
+ return [
+ name,
+ addr.company,
+ addr.address_1,
+ addr.address_2,
+ addr.city,
+ addr.state,
+ addr.postcode,
+ addr.country,
+ addr.phone
+ ].filter(Boolean).join(', ');
+ }
+ private mapOrder(item: WooOrder): UnifiedOrderDTO {
+ // 将 WooCommerce 订单数据映射为统一订单DTO
+ // 包含账单地址与收货地址以及创建与更新时间
+ return {
+ id: item.id,
+ number: item.number,
+ status: item.status,
+ currency: item.currency,
+ total: item.total,
+ customer_id: item.customer_id,
+ customer_name: `${item.billing?.first_name || ''} ${
+ item.billing?.last_name || ''
+ }`.trim(),
+ refunds: item.refunds?.map?.(refund => ({
+ id: refund.id,
+ reason: refund.reason,
+ total: refund.total,
+ })),
+ email: item.billing?.email || '',
+ line_items: (item.line_items as any[]).map(li => ({
+ ...li,
+ productId: li.product_id,
+ })),
+
+ billing: item.billing,
+ shipping: item.shipping,
+ billing_full_address: this.buildFullAddress(item.billing),
+ shipping_full_address: this.buildFullAddress(item.shipping),
+ payment_method: item.payment_method_title,
+ date_created: item.date_created,
+ date_modified: item.date_modified,
+ raw: item,
+ };
+ }
+
+ 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,
+ raw: item,
+ };
+ }
+
+ private mapMedia(item: WpMedia): UnifiedMediaDTO {
+ // 将 WordPress 媒体数据映射为统一媒体DTO
+ // 兼容不同字段命名的时间信息
+ return {
+ id: item.id,
+ 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 ?? item.date,
+ date_modified: item.date_modified ?? item.modified,
+ };
+ }
+
+ async getProducts(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ // 获取产品列表并使用统一分页结构返回
+ const requestParams = this.mapProductSearchParams(params);
+ const { items, total, totalPages, page, per_page } =
+ await this.wpService.fetchResourcePaged(
+ this.site,
+ 'products',
+ requestParams
+ );
+ return {
+ items: items.map(this.mapProduct),
+ total,
+ totalPages,
+ page,
+ 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 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 });
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ async batchProcessProducts(
+ data: { create?: any[]; update?: any[]; delete?: Array }
+ ): Promise {
+ // 批量处理产品增删改
+ return await this.wpService.batchProcessProducts(this.site, data);
+ }
+
+ async getOrders(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ const requestParams = this.mapOrderSearchParams(params);
+ const { items, total, totalPages, page, per_page } =
+ await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams);
+ return {
+ items: items.map(this.mapOrder),
+ total,
+ totalPages,
+ page,
+ 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;
+ }
+
+ async shipOrder(orderId: string | number, data: {
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: Array<{
+ order_item_id: number;
+ quantity: number;
+ }>;
+ }): Promise {
+ throw new Error('暂无实现')
+ // 订单发货
+ // const api = (this.wpService as any).createApi(this.site, 'wc/v3');
+
+ // try {
+ // // 更新订单状态为已完成
+ // await api.put(`orders/${orderId}`, { status: 'completed' });
+
+ // // 如果提供了物流信息,添加到订单备注
+ // if (data.tracking_number || data.shipping_provider) {
+ // const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`;
+ // await api.post(`orders/${orderId}/notes`, { note, customer_note: true });
+ // }
+
+ // return {
+ // success: true,
+ // order_id: orderId,
+ // shipment_id: `shipment_${orderId}_${Date.now()}`,
+ // tracking_number: data.tracking_number,
+ // shipping_provider: data.shipping_provider,
+ // shipped_at: new Date().toISOString()
+ // };
+ // } catch (error) {
+ // throw new Error(`发货失败: ${error.message}`);
+ // }
+ }
+
+ async cancelShipOrder(orderId: string | number, data: {
+ reason?: string;
+ shipment_id?: string;
+ }): Promise {
+ throw new Error('暂未实现')
+ // 取消订单发货
+ // const api = (this.wpService as any).createApi(this.site, 'wc/v3');
+
+ // try {
+ // // 将订单状态改回处理中
+ // await api.put(`orders/${orderId}`, { status: 'processing' });
+
+ // // 添加取消发货的备注
+ // const note = `订单发货已取消${data.reason ? `,原因:${data.reason}` : ''}`;
+ // await api.post(`orders/${orderId}/notes`, { note, customer_note: true });
+
+ // return {
+ // success: true,
+ // order_id: orderId,
+ // shipment_id: data.shipment_id,
+ // reason: data.reason,
+ // cancelled_at: new Date().toISOString()
+ // };
+ // } catch (error) {
+ // throw new Error(`取消发货失败: ${error.message}`);
+ // }
+ }
+
+ async getSubscriptions(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ // 获取订阅列表并映射为统一订阅DTO集合
+ const { items, total, totalPages, page, per_page } =
+ await this.wpService.fetchResourcePaged(
+ this.site,
+ 'subscriptions',
+ params
+ );
+ return {
+ items: items.map(this.mapSubscription),
+ total,
+ totalPages,
+ page,
+ per_page,
+
+ };
+ }
+
+ async getMedia(
+ params: UnifiedSearchParamsDTO
+ ): Promise> {
+ // 获取媒体列表并映射为统一媒体DTO集合
+ const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
+ this.site,
+ params
+ );
+ return {
+ items: items.map(this.mapMedia.bind(this)),
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ private mapReview(item: any): UnifiedReviewDTO & {raw: any} {
+ // 将 WooCommerce 评论数据映射为统一评论DTO
+ return {
+ id: item.id,
+ product_id: item.product_id,
+ author: item.reviewer,
+ email: item.reviewer_email,
+ content: item.review,
+ rating: item.rating,
+ status: item.status,
+ date_created: item.date_created,
+ raw: item
+ };
+ }
+
+ async getReviews(
+ params: UnifiedSearchParamsDTO
+ ): Promise {
+ // 获取评论列表并使用统一分页结构返回
+ const requestParams = this.mapProductSearchParams(params);
+ const { items, total, totalPages, page, per_page } =
+ await this.wpService.fetchResourcePaged(
+ this.site,
+ 'products/reviews',
+ requestParams
+ );
+ return {
+ items: items.map(this.mapReview.bind(this)),
+ total,
+ totalPages,
+ page,
+ per_page,
+ };
+ }
+
+ async createReview(data: any): Promise {
+ const res = await this.wpService.createReview(this.site, data);
+ return this.mapReview(res);
+ }
+
+ async updateReview(id: number, data: any): Promise {
+ const res = await this.wpService.updateReview(this.site, id, data);
+ return this.mapReview(res);
+ }
+
+ async deleteReview(id: number): Promise {
+ return await this.wpService.deleteReview(this.site, id);
+ }
+
+ 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);
+ }
+
+ async convertMediaToWebp(ids: Array): Promise<{ converted: any[]; failed: any[] }> {
+ // 函数说明 调用服务层将站点的指定媒体批量转换为 webp 并上传
+ const result = await this.wpService.convertMediaToWebp(Number(this.site.id), ids);
+ return result as any;
+ }
+
+ 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,
+ };
+ }
+
+ async getCustomers(params: UnifiedSearchParamsDTO): Promise> {
+ const requestParams = this.mapCustomerSearchParams(params);
+ const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(
+ this.site,
+ 'customers',
+ requestParams
+ );
+ return {
+ items: items.map((i: any) => this.mapCustomer(i)),
+ total,
+ totalPages,
+ page,
+ per_page,
+
+ };
+ }
+
+ async getCustomer(id: string | number): Promise {
+ const api = (this.wpService as any).createApi(this.site, 'wc/v3');
+ const res = await api.get(`customers/${id}`);
+ return this.mapCustomer(res.data);
+ }
+
+ async createCustomer(data: Partial): Promise {
+ const api = (this.wpService as any).createApi(this.site, 'wc/v3');
+ const res = await api.post('customers', data);
+ return this.mapCustomer(res.data);
+ }
+
+ async updateCustomer(id: string | number, data: Partial): Promise {
+ const api = (this.wpService as any).createApi(this.site, 'wc/v3');
+ const res = await api.put(`customers/${id}`, data);
+ return this.mapCustomer(res.data);
+ }
+
+ async deleteCustomer(id: string | number): Promise {
+ const api = (this.wpService as any).createApi(this.site, 'wc/v3');
+ await api.delete(`customers/${id}`, { force: true });
+ return true;
+ }
+}
diff --git a/src/config/config.default.ts b/src/config/config.default.ts
index fd99d08..3314dfe 100644
--- a/src/config/config.default.ts
+++ b/src/config/config.default.ts
@@ -1,6 +1,6 @@
import { MidwayConfig } from '@midwayjs/core';
+import { join } from 'path';
import { Product } from '../entity/product.entity';
-import { Category } from '../entity/category.entity';
import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity';
import { User } from '../entity/user.entity';
@@ -11,12 +11,12 @@ import { StockPoint } from '../entity/stock_point.entity';
import { StockRecord } from '../entity/stock_record.entity';
import { Order } from '../entity/order.entity';
import { OrderItem } from '../entity/order_item.entity';
-import { OrderCoupon } from '../entity/order_copon.entity';
+import { OrderCoupon } from '../entity/order_coupon.entity';
import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity';
-import { OrderRefundItem } from '../entity/order_retund_item.entity';
+import { OrderRefundItem } from '../entity/order_refund_item.entity';
import { OrderSale } from '../entity/order_sale.entity';
-import { OrderSaleOriginal } from '../entity/order_item_original.entity';
+import { OrderItemOriginal } from '../entity/order_item_original.entity';
import { OrderShipping } from '../entity/order_shipping.entity';
import { Service } from '../entity/service.entity';
import { ShippingAddress } from '../entity/shipping_address.entity';
@@ -26,14 +26,23 @@ import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity';
import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity';
-import { Strength } from '../entity/strength.entity';
-import { Flavors } from '../entity/flavors.entity';
import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist';
import { AuthCode } from '../entity/auth_code';
import { Subscription } from '../entity/subscription.entity';
import { Site } from '../entity/site.entity';
+import { Dict } from '../entity/dict.entity';
+import { DictItem } from '../entity/dict_item.entity';
+import { Template } from '../entity/template.entity';
+import { Area } from '../entity/area.entity';
+import { ProductStockComponent } from '../entity/product_stock_component.entity';
+import { ProductSiteSku } from '../entity/product_site_sku.entity';
+import { CategoryAttribute } from '../entity/category_attribute.entity';
+import { Category } from '../entity/category.entity';
+import DictSeeder from '../db/seeds/dict.seeder';
+import CategorySeeder from '../db/seeds/category.seeder';
+import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder';
export default {
// use for cookie sign key, should change to your own and keep security
@@ -42,9 +51,8 @@ export default {
default: {
entities: [
Product,
- Category,
- Strength,
- Flavors,
+ ProductStockComponent,
+ ProductSiteSku,
WpProduct,
Variation,
User,
@@ -60,7 +68,7 @@ export default {
OrderRefund,
OrderRefundItem,
OrderSale,
- OrderSaleOriginal,
+ OrderItemOriginal,
OrderShipment,
ShipmentItem,
Shipment,
@@ -76,15 +84,22 @@ export default {
AuthCode,
Subscription,
Site,
+ Dict,
+ DictItem,
+ Template,
+ Area,
+ CategoryAttribute,
+ Category,
],
synchronize: true,
logging: false,
+ seeders: [DictSeeder, CategorySeeder, CategoryAttributeSeeder],
},
dataSource: {
default: {
type: 'mysql',
host: 'localhost',
- port: 3306,
+ port: 10014,
username: 'root',
password: 'root',
database: 'inventory',
@@ -95,7 +110,7 @@ export default {
// origin: '*', // 允许所有来源跨域请求
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
// allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
- // credentials: true, // 允许携带凭据(cookies等)
+ // credentials: true, // 允许携带凭据(cookies等)
// },
// jwt: {
// secret: 'YOONE2024!@abc',
@@ -107,7 +122,7 @@ export default {
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
- // siteName: 'Local',
+ // name: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: '',
// },
@@ -128,5 +143,16 @@ export default {
user: 'info@canpouches.com',
pass: 'WWqQ4aZq4Jrm9uwz',
},
-}
+ },
+ upload: {
+ // mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
+ mode: 'file',
+ fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
+ whitelist: ['.csv'], // 支持的文件后缀
+ tmpdir: join(__dirname, '../../tmp_uploads'),
+ cleanTimeout: 5 * 60 * 1000,
+ },
+ koa: {
+ queryParseMode: 'extended',
+ },
} as MidwayConfig;
diff --git a/src/configuration.ts b/src/configuration.ts
index e413b12..3bd7813 100644
--- a/src/configuration.ts
+++ b/src/configuration.ts
@@ -9,13 +9,15 @@ import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info';
import * as orm from '@midwayjs/typeorm';
import { join } from 'path';
-// import { DefaultErrorFilter } from './filter/default.filter';
-// import { NotFoundFilter } from './filter/notfound.filter';
+import { DefaultErrorFilter } from './filter/default.filter';
+import { NotFoundFilter } from './filter/notfound.filter';
import { ReportMiddleware } from './middleware/report.middleware';
+import { QueryNormalizeMiddleware } from './middleware/query-normalize.middleware';
import * as swagger from '@midwayjs/swagger';
import * as crossDomain from '@midwayjs/cross-domain';
import * as cron from '@midwayjs/cron';
import * as jwt from '@midwayjs/jwt';
+import * as upload from '@midwayjs/upload';
import { USER_KEY } from './decorator/user.decorator';
import { SiteService } from './service/site.service';
import { AuthMiddleware } from './middleware/auth.middleware';
@@ -33,6 +35,7 @@ import { AuthMiddleware } from './middleware/auth.middleware';
crossDomain,
cron,
jwt,
+ upload,
],
importConfigs: [join(__dirname, './config')],
})
@@ -51,9 +54,9 @@ export class MainConfiguration {
async onReady() {
// add middleware
- this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
+ this.app.useMiddleware([QueryNormalizeMiddleware, ReportMiddleware, AuthMiddleware]);
// add filter
- // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
+ this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
this.decoratorService.registerParameterHandler(
USER_KEY,
diff --git a/src/controller/area.controller.ts b/src/controller/area.controller.ts
new file mode 100644
index 0000000..4c28193
--- /dev/null
+++ b/src/controller/area.controller.ts
@@ -0,0 +1,118 @@
+
+import { Body, Context, Controller, Del, Get, Inject, Param, Post, Put, Query } from '@midwayjs/core';
+import {
+ ApiBearerAuth,
+ ApiBody,
+ ApiExtension,
+ ApiOkResponse,
+ ApiOperation,
+ ApiTags,
+} from '@midwayjs/swagger';
+import { AreaService } from '../service/area.service';
+import { CreateAreaDTO, QueryAreaDTO, UpdateAreaDTO } from '../dto/area.dto';
+import { errorResponse, successResponse } from '../utils/response.util';
+import { Area } from '../entity/area.entity';
+import * as countries from 'i18n-iso-countries';
+
+@ApiBearerAuth()
+@ApiTags('Area')
+@Controller('/area')
+export class AreaController {
+ @Inject()
+ ctx: Context;
+
+ @Inject()
+ areaService: AreaService;
+
+ @ApiOperation({ summary: '获取国家列表' })
+ @ApiOkResponse({ description: '国家列表' })
+ @Get('/countries')
+ async getCountries() {
+ try {
+ // 注册中文语言包
+ countries.registerLocale(require('i18n-iso-countries/langs/zh.json'));
+ // 获取所有国家的中文名称
+ const countryNames = countries.getNames('zh', { select: 'official' });
+ // 格式化为 { code, name } 的数组
+ const countryList = Object.keys(countryNames).map(code => ({
+ code,
+ name: countryNames[code],
+ }));
+ return successResponse(countryList, '查询成功');
+ } catch (error) {
+ console.log(error);
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOperation({ summary: '创建区域' })
+ @ApiBody({ type: CreateAreaDTO })
+ @ApiOkResponse({ type: Area, description: '成功创建的区域' })
+ @Post('/')
+ async createArea(@Body() area: CreateAreaDTO) {
+ try {
+ const newArea = await this.areaService.createArea(area);
+ return successResponse(newArea, '创建成功');
+ } catch (error) {
+ console.log(error);
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOperation({ summary: '更新区域' })
+ @ApiBody({ type: UpdateAreaDTO })
+ @ApiOkResponse({ type: Area, description: '成功更新的区域' })
+ @Put('/:id')
+ async updateArea(@Param('id') id: number, @Body() area: UpdateAreaDTO) {
+ try {
+ const updatedArea = await this.areaService.updateArea(id, area);
+ return successResponse(updatedArea, '更新成功');
+ } catch (error) {
+ console.log(error);
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOperation({ summary: '删除区域' })
+ @ApiOkResponse({ description: '删除成功' })
+ @Del('/:id')
+ async deleteArea(@Param('id') id: number) {
+ try {
+ await this.areaService.deleteArea(id);
+ return successResponse(null, '删除成功');
+ } catch (error) {
+ console.log(error);
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOperation({ summary: '获取区域列表(分页)' })
+ @ApiOkResponse({ type: [Area], description: '区域列表' })
+ @ApiExtension('x-pagination', { currentPage: 1, pageSize: 10, total: 100 })
+ @Get('/')
+ async getAreaList(@Query() query: QueryAreaDTO) {
+ try {
+ const { list, total } = await this.areaService.getAreaList(query);
+ return successResponse({ list, total }, '查询成功');
+ } catch (error) {
+ console.log(error);
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOperation({ summary: '根据ID获取区域详情' })
+ @ApiOkResponse({ type: Area, description: '区域详情' })
+ @Get('/:id')
+ async getAreaById(@Param('id') id: number) {
+ try {
+ const area = await this.areaService.getAreaById(id);
+ if (!area) {
+ return errorResponse('区域不存在');
+ }
+ return successResponse(area, '查询成功');
+ } catch (error) {
+ console.log(error);
+ return errorResponse(error?.message || error);
+ }
+ }
+}
diff --git a/src/controller/category.controller.ts b/src/controller/category.controller.ts
new file mode 100644
index 0000000..e8b625f
--- /dev/null
+++ b/src/controller/category.controller.ts
@@ -0,0 +1,98 @@
+import { Controller, Get, Post, Put, Del, Body, Query, Inject, Param } from '@midwayjs/core';
+import { CategoryService } from '../service/category.service';
+import { successResponse, errorResponse } from '../utils/response.util';
+import { ApiOkResponse } from '@midwayjs/swagger';
+
+@Controller('/category')
+export class CategoryController {
+ @Inject()
+ categoryService: CategoryService;
+
+ @ApiOkResponse()
+ @Get('/all')
+ async getAll() {
+ try {
+ const data = await this.categoryService.getAll();
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Get('/')
+ async getList(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
+ try {
+ const data = await this.categoryService.getList({ current, pageSize }, name);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Post('/')
+ async create(@Body() body: any) {
+ try {
+ const data = await this.categoryService.create(body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Put('/:id')
+ async update(@Param('id') id: number, @Body() body: any) {
+ try {
+ const data = await this.categoryService.update(id, body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Del('/:id')
+ async delete(@Param('id') id: number) {
+ try {
+ await this.categoryService.delete(id);
+ return successResponse(null);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Get('/attribute/:categoryId')
+ async getCategoryAttributes(@Param('categoryId') categoryId: number) {
+ try {
+ const data = await this.categoryService.getCategoryAttributes(categoryId);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Post('/attribute')
+ async createCategoryAttribute(@Body() body: { categoryId: number, attributeDictIds: number[] }) {
+ try {
+ const data = await this.categoryService.createCategoryAttribute(body.categoryId, body.attributeDictIds);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Del('/attribute/:id')
+ async deleteCategoryAttribute(@Param('id') id: number) {
+ try {
+ await this.categoryService.deleteCategoryAttribute(id);
+ return successResponse(null);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+}
diff --git a/src/controller/customer.controller.ts b/src/controller/customer.controller.ts
index 151d179..9225aa5 100644
--- a/src/controller/customer.controller.ts
+++ b/src/controller/customer.controller.ts
@@ -1,82 +1,66 @@
-import {
- Body,
- Context,
- Controller,
- Del,
- Get,
- Inject,
- Post,
- Put,
- Query,
-} from '@midwayjs/core';
+import { Controller, Get, Post, Inject, Query, Body } from '@midwayjs/core';
+import { successResponse, errorResponse } from '../utils/response.util';
import { CustomerService } from '../service/customer.service';
-import { errorResponse, successResponse } from '../utils/response.util';
+import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
-import { BooleanRes } from '../dto/reponse.dto';
-import { CustomerTagDTO, QueryCustomerListDTO } from '../dto/customer.dto';
@Controller('/customer')
export class CustomerController {
- @Inject()
- ctx: Context;
-
@Inject()
customerService: CustomerService;
- @ApiOkResponse()
- @Get('/list')
- async getCustomerList(@Query() param: QueryCustomerListDTO) {
+ @ApiOkResponse({ type: Object })
+ @Get('/getcustomerlist')
+ async getCustomerList(@Query() query: QueryCustomerListDTO) {
try {
- const data = await this.customerService.getCustomerList(param);
- return successResponse(data);
+ const result = await this.customerService.getCustomerList(query as any);
+ return successResponse(result);
} catch (error) {
- console.log(error)
- return errorResponse(error?.message || error);
+ return errorResponse(error.message);
}
}
- @ApiOkResponse({ type: BooleanRes })
- @Post('/tag/add')
- async addTag(@Body() dto: CustomerTagDTO) {
+ @ApiOkResponse({ type: Object })
+ @Post('/addtag')
+ async addTag(@Body() body: CustomerTagDTO) {
try {
- await this.customerService.addTag(dto.email, dto.tag);
- return successResponse(true);
+ const result = await this.customerService.addTag(body.email, body.tag);
+ return successResponse(result);
} catch (error) {
- return errorResponse(error?.message || error);
+ return errorResponse(error.message);
}
}
- @ApiOkResponse({ type: BooleanRes })
- @Del('/tag/del')
- async delTag(@Body() dto: CustomerTagDTO) {
+ @ApiOkResponse({ type: Object })
+ @Post('/deltag')
+ async delTag(@Body() body: CustomerTagDTO) {
try {
- await this.customerService.delTag(dto.email, dto.tag);
- return successResponse(true);
+ const result = await this.customerService.delTag(body.email, body.tag);
+ return successResponse(result);
} catch (error) {
- return errorResponse(error?.message || error);
+ return errorResponse(error.message);
}
}
- @ApiOkResponse()
- @Get('/tags')
+ @ApiOkResponse({ type: Object })
+ @Get('/gettags')
async getTags() {
try {
- const data = await this.customerService.getTags();
- return successResponse(data);
+ const result = await this.customerService.getTags();
+ return successResponse(result);
} catch (error) {
- return errorResponse(error?.message || error);
+ return errorResponse(error.message);
}
}
-
- @ApiOkResponse({ type: BooleanRes })
- @Put('/rate')
- async setRate(@Body() params: { id: number; rate: number }) {
+ @ApiOkResponse({ type: Object })
+ @Post('/setrate')
+ async setRate(@Body() body: { id: number; rate: number }) {
try {
- await this.customerService.setRate(params);
- return successResponse(true);
+ const result = await this.customerService.setRate({ id: body.id, rate: body.rate });
+ return successResponse(result);
} catch (error) {
- return errorResponse(error?.message || error);
+ return errorResponse(error.message);
}
}
}
diff --git a/src/controller/dict.controller.ts b/src/controller/dict.controller.ts
new file mode 100644
index 0000000..8413ca2
--- /dev/null
+++ b/src/controller/dict.controller.ts
@@ -0,0 +1,221 @@
+
+import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param, Files, ContentType } from '@midwayjs/core';
+import { DictService } from '../service/dict.service';
+import { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
+import { Validate } from '@midwayjs/validate';
+import { Context } from '@midwayjs/koa';
+import { successResponse, errorResponse } from '../utils/response.util';
+
+/**
+ * 字典管理
+ * @decorator Controller
+ */
+@Controller('/dict')
+export class DictController {
+ @Inject()
+ dictService: DictService;
+
+ @Inject()
+ ctx: Context;
+
+ /**
+ * 批量导入字典
+ * @param files 上传的文件
+ */
+ @Post('/import')
+ @Validate()
+ async importDicts(@Files() files: any) {
+ // 从上传的文件列表中获取第一个文件
+ const file = files[0];
+ // 调用服务层方法处理XLSX文件
+ const result = await this.dictService.importDictsFromXLSX(file.data);
+ // 返回导入结果
+ return result;
+ }
+
+ /**
+ * 下载字典XLSX模板
+ */
+ @Get('/template')
+ @ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ async downloadDictTemplate() {
+ // 设置下载文件的名称
+ this.ctx.set('Content-Disposition', 'attachment; filename=dict-template.xlsx');
+ // 返回XLSX模板内容
+ return this.dictService.getDictXLSXTemplate();
+ }
+
+ /**
+ * 获取单个字典及其所有字典项
+ * @param id 字典ID
+ */
+ @Get('/:id')
+ async getDict(@Param('id') id: number) {
+ // 调用服务层方法,并关联查询字典项
+ return this.dictService.getDict({ id }, ['items']);
+ }
+
+ /**
+ * 获取字典列表
+ * @param title 字典标题 (模糊查询)
+ * @param name 字典名称 (模糊查询)
+ */
+ @Get('/list')
+ async getDicts(@Query('title') title?: string, @Query('name') name?: string) {
+ // 调用服务层方法
+ return this.dictService.getDicts({ title, name });
+ }
+
+ /**
+ * 创建新字典
+ * @param createDictDTO 字典数据
+ */
+ @Post('/')
+ @Validate()
+ async createDict(@Body() createDictDTO: CreateDictDTO) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.createDict(createDictDTO);
+ return successResponse(result, '字典创建成功');
+ } catch (error) {
+ return errorResponse(error?.message || '字典创建失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 更新字典
+ * @param id 字典ID
+ * @param updateDictDTO 待更新的字典数据
+ */
+ @Put('/:id')
+ @Validate()
+ async updateDict(@Param('id') id: number, @Body() updateDictDTO: UpdateDictDTO) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.updateDict(id, updateDictDTO);
+ return successResponse(result, '字典更新成功');
+ } catch (error) {
+ return errorResponse(error?.message || '字典更新失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 删除字典
+ * @param id 字典ID
+ */
+ @Del('/:id')
+ async deleteDict(@Param('id') id: number) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.deleteDict(id);
+ return successResponse(result, '字典删除成功');
+ } catch (error) {
+ return errorResponse(error?.message || '字典删除失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 批量导入字典项
+ * @param files 上传的文件
+ * @param body 请求体,包含字典ID
+ */
+ @Post('/item/import')
+ @Validate()
+ async importDictItems(@Files() files: any, @Body() body: { dictId: number }) {
+ // 获取第一个文件
+ const file = files[0];
+ // 调用服务层方法
+ const result = await this.dictService.importDictItemsFromXLSX(file.data, body.dictId);
+ // 返回结果
+ return result;
+ }
+
+ /**
+ * 下载字典项XLSX模板
+ */
+ @Get('/item/template')
+ @ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ async downloadDictItemTemplate() {
+ // 设置下载文件名
+ this.ctx.set('Content-Disposition', 'attachment; filename=dict-item-template.xlsx');
+ // 返回模板内容
+ return this.dictService.getDictItemXLSXTemplate();
+ }
+
+ /**
+ * 获取字典项列表
+ * @param dictId 字典ID (可选)
+ */
+ @Get('/items')
+ async getDictItems(
+ @Query('dictId') dictId?: number,
+ @Query('name') name?: string,
+ @Query('title') title?: string,
+ ) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.getDictItems({ dictId, name, title });
+ return successResponse(result, '获取字典项列表成功');
+ } catch (error) {
+ return errorResponse(error?.message || '获取字典项列表失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 创建新字典项
+ * @param createDictItemDTO 字典项数据
+ */
+ @Post('/item')
+ @Validate()
+ async createDictItem(@Body() createDictItemDTO: CreateDictItemDTO) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.createDictItem(createDictItemDTO);
+ return successResponse(result, '字典项创建成功');
+ } catch (error) {
+ return errorResponse(error?.message || '字典项创建失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 更新字典项
+ * @param id 字典项ID
+ * @param updateDictItemDTO 待更新的字典项数据
+ */
+ @Put('/item/:id')
+ @Validate()
+ async updateDictItem(@Param('id') id: number, @Body() updateDictItemDTO: UpdateDictItemDTO) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.updateDictItem(id, updateDictItemDTO);
+ return successResponse(result, '字典项更新成功');
+ } catch (error) {
+ return errorResponse(error?.message || '字典项更新失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 删除字典项
+ * @param id 字典项ID
+ */
+ @Del('/item/:id')
+ async deleteDictItem(@Param('id') id: number) {
+ try {
+ // 调用服务层方法
+ const result = await this.dictService.deleteDictItem(id);
+ return successResponse(result, '字典项删除成功');
+ } catch (error) {
+ return errorResponse(error?.message || '字典项删除失败', error?.code || 500);
+ }
+ }
+
+ /**
+ * 根据字典名称获取字典项列表
+ * @param name 字典名称
+ */
+ @Get('/items-by-name')
+ async getDictItemsByDictName(@Query('name') name: string) {
+ // 调用服务层方法
+ return this.dictService.getDictItemsByDictName(name);
+ }
+}
diff --git a/src/controller/locale.controller.ts b/src/controller/locale.controller.ts
new file mode 100644
index 0000000..3377512
--- /dev/null
+++ b/src/controller/locale.controller.ts
@@ -0,0 +1,36 @@
+
+import { Controller, Get, Inject, Param } from '@midwayjs/core';
+import { DictService } from '../service/dict.service';
+
+/**
+ * 国际化语言包
+ */
+@Controller('/locales')
+export class LocaleController {
+ @Inject()
+ dictService: DictService;
+
+ /**
+ * 根据语言获取所有翻译项
+ * @param lang 语言代码,例如 zh-CN, en-US
+ * @returns 返回一个包含所有翻译键值对的 JSON 对象
+ */
+ @Get('/:lang')
+ async getLocale(@Param('lang') lang: string) {
+ // 根据语言代码查找对应的字典
+ const dict = await this.dictService.getDict({ name: lang }, ['items']);
+
+ // 如果字典不存在,则返回空对象
+ if (!dict) {
+ return {};
+ }
+
+ // 将字典项转换为 key-value 对象
+ const locale = dict.items.reduce((acc, item) => {
+ acc[item.name] = item.title;
+ return acc;
+ }, {});
+
+ return locale;
+ }
+}
diff --git a/src/controller/media.controller.ts b/src/controller/media.controller.ts
new file mode 100644
index 0000000..cafd85f
--- /dev/null
+++ b/src/controller/media.controller.ts
@@ -0,0 +1,79 @@
+import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core';
+import { WPService } from '../service/wp.service';
+import { successResponse, errorResponse } from '../utils/response.util';
+
+@Controller('/media')
+export class MediaController {
+ @Inject()
+ wpService: WPService;
+
+ @Get('/list')
+ async list(
+ @Query('siteId') siteId: number,
+ @Query('page') page: number = 1,
+ @Query('pageSize') pageSize: number = 20
+ ) {
+ try {
+ if (!siteId) {
+ return errorResponse('siteId is required');
+ }
+ const result = await this.wpService.getMedia(siteId, page, pageSize);
+ return successResponse(result);
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/upload')
+ async upload(@Fields() fields, @Files() files) {
+ try {
+ const siteId = fields.siteId;
+ if (!siteId) {
+ return errorResponse('siteId is required');
+ }
+ if (!files || files.length === 0) {
+ return errorResponse('file is required');
+ }
+ const file = files[0];
+ const result = await this.wpService.createMedia(siteId, file);
+ return successResponse(result);
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/update/:id')
+ async update(@Param('id') id: number, @Body() body) {
+ try {
+ const siteId = body.siteId;
+ if (!siteId) {
+ return errorResponse('siteId is required');
+ }
+ // 过滤出需要更新的字段
+ const { title, caption, description, alt_text } = body;
+ const data: any = {};
+ if (title !== undefined) data.title = title;
+ if (caption !== undefined) data.caption = caption;
+ if (description !== undefined) data.description = description;
+ if (alt_text !== undefined) data.alt_text = alt_text;
+
+ const result = await this.wpService.updateMedia(siteId, id, data);
+ return successResponse(result);
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:id')
+ async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) {
+ try {
+ if (!siteId) {
+ return errorResponse('siteId is required');
+ }
+ const result = await this.wpService.deleteMedia(siteId, id, force);
+ return successResponse(result);
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+}
diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts
index cdeb3ad..8aa7b5f 100644
--- a/src/controller/order.controller.ts
+++ b/src/controller/order.controller.ts
@@ -36,10 +36,10 @@ export class OrderController {
type: BooleanRes,
})
@Post('/syncOrder/:siteId')
- async syncOrder(@Param('siteId') siteId: string) {
+ async syncOrder(@Param('siteId') siteId: number, @Body() params: Record) {
try {
- await this.orderService.syncOrders(siteId);
- return successResponse(true);
+ const result = await this.orderService.syncOrders(siteId, params);
+ return successResponse(result);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
@@ -51,7 +51,7 @@ export class OrderController {
})
@Post('/syncOrder/:siteId/order/:orderId')
async syncOrderById(
- @Param('siteId') siteId: string,
+ @Param('siteId') siteId: number,
@Param('orderId') orderId: string
) {
try {
diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts
index cfdedb6..bc500cf 100644
--- a/src/controller/product.controller.ts
+++ b/src/controller/product.controller.ts
@@ -11,36 +11,19 @@ import {
} from '@midwayjs/core';
import { ProductService } from '../service/product.service';
import { errorResponse, successResponse } from '../utils/response.util';
-import {
- BatchSetSkuDTO,
- CreateCategoryDTO,
- CreateFlavorsDTO,
- CreateProductDTO,
- CreateStrengthDTO,
- QueryCategoryDTO,
- QueryFlavorsDTO,
- QueryProductDTO,
- QueryStrengthDTO,
- UpdateCategoryDTO,
- UpdateFlavorsDTO,
- UpdateProductDTO,
- UpdateStrengthDTO,
-} from '../dto/product.dto';
+import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, BatchUpdateProductDTO, BatchDeleteProductDTO } from '../dto/product.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
-import {
- BooleanRes,
- ProductCatListRes,
- ProductCatRes,
- ProductListRes,
- ProductRes,
- ProductsRes,
-} from '../dto/reponse.dto';
+import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
+import { ContentType, Files } from '@midwayjs/core';
+import { Context } from '@midwayjs/koa';
@Controller('/product')
export class ProductController {
@Inject()
productService: ProductService;
- ProductRes;
+
+ @Inject()
+ ctx: Context;
@ApiOkResponse({
description: '通过name搜索产品',
@@ -79,12 +62,14 @@ export class ProductController {
async getProductList(
@Query() query: QueryProductDTO
): Promise {
- const { current = 1, pageSize = 10, name, categoryId } = query;
+ const { current = 1, pageSize = 10, name, brandId, sortField, sortOrder } = query;
try {
const data = await this.productService.getProductList(
{ current, pageSize },
name,
- categoryId
+ brandId,
+ sortField,
+ sortOrder
);
return successResponse(data);
} catch (error) {
@@ -93,56 +78,146 @@ export class ProductController {
}
}
- @ApiOkResponse({
- type: ProductRes,
- })
+ @ApiOkResponse({ type: ProductRes })
@Post('/')
async createProduct(@Body() productData: CreateProductDTO) {
try {
- const data = this.productService.createProduct(productData);
+ const data = await this.productService.createProduct(productData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @ApiOkResponse({
- type: ProductRes,
- })
+ // 导出所有产品 CSV
+ @ApiOkResponse()
+ @Get('/export')
+ @ContentType('text/csv')
+ async exportProductsCSV() {
+ try {
+ const csv = await this.productService.exportProductsCSV();
+ // 设置下载文件名(附件形式)
+ const date = new Date();
+ const pad = (n: number) => String(n).padStart(2, '0');
+ const name = `products-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}.csv`;
+ this.ctx.set('Content-Disposition', `attachment; filename=${name}`);
+ return csv;
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 导入产品(CSV 文件)
+ @ApiOkResponse()
+ @Post('/import')
+ async importProductsCSV(@Files() files: any) {
+ try {
+ // 条件判断:确保存在文件
+ const file = files?.[0];
+ if (!file) return errorResponse('未接收到上传文件');
+
+ const result = await this.productService.importProductsCSV(file);
+ return successResponse(result);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse({ type: ProductRes })
@Put('/:id')
- async updateProduct(
- @Param('id') id: number,
- @Body() productData: UpdateProductDTO
- ) {
+ async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
try {
- const data = this.productService.updateProduct(id, productData);
+ const data = await this.productService.updateProduct(id, productData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @ApiOkResponse({
- type: ProductRes,
- })
+ @ApiOkResponse({ type: BooleanRes })
+ @Put('/batch-update')
+ async batchUpdateProduct(@Body() batchUpdateProductDTO: BatchUpdateProductDTO) {
+ try {
+ await this.productService.batchUpdateProduct(batchUpdateProductDTO);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse({ type: BooleanRes })
+ @Post('/batch-delete')
+ async batchDeleteProduct(@Body() body: BatchDeleteProductDTO) {
+ try {
+ const result = await this.productService.batchDeleteProduct(body.ids);
+ if (result.failed > 0) {
+ return errorResponse(`成功删除 ${result.success} 个,失败 ${result.failed} 个。首个错误: ${result.errors[0]}`);
+ }
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse({ type: ProductRes })
@Put('updateNameCn/:id/:nameCn')
- async updateProductNameCn(
- @Param('id') id: number,
- @Param('nameCn') nameCn: string
- ) {
+ async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
try {
- const data = this.productService.updateProductNameCn(id, nameCn);
+ const data = await this.productService.updatenameCn(id, nameCn);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
-
+ // 根据站点SKU查询产品
+ @ApiOkResponse({ type: ProductRes })
+ @Get('/site-sku/:siteSku')
+ async getProductBySiteSku(@Param('siteSku') siteSku: string) {
+ try {
+ const product = await this.productService.findProductBySiteSku(siteSku);
+ return successResponse(product);
+ } catch (error) {
+ return errorResponse(error.message || '获取数据失败');
+ }
+ }
- @ApiOkResponse({
- type: BooleanRes,
- })
+ // 获取产品的站点SKU绑定
+ @ApiOkResponse()
+ @Get('/:id/site-skus')
+ async getProductSiteSkus(@Param('id') id: number) {
+ try {
+ const data = await this.productService.getProductSiteSkus(id);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 覆盖式绑定产品的站点SKU列表
+ @ApiOkResponse()
+ @Post('/:id/site-skus')
+ async bindProductSiteSkus(@Param('id') id: number, @Body() body: { codes: string[] }) {
+ try {
+ const data = await this.productService.bindSiteSkus(id, body?.codes || []);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse({ type: ProductRes })
+ @Get('/:id')
+ async getProductById(@Param('id') id: number) {
+ try {
+ const product = await this.productService.getProductById(id);
+ return successResponse(product);
+ } catch (error) {
+ return errorResponse(error.message || '获取数据失败');
+ }
+ }
+
+ @ApiOkResponse({ type: BooleanRes })
@Del('/:id')
async deleteProduct(@Param('id') id: number) {
try {
@@ -153,14 +228,58 @@ export class ProductController {
}
}
- @ApiOkResponse({
- type: ProductCatListRes,
- })
- @Get('/categories')
- async getCategories(@Query() query: QueryCategoryDTO) {
- const { current = 1, pageSize = 10, name } = query;
+ // 获取产品的库存组成
+ @ApiOkResponse()
+ @Get('/:id/components')
+ async getProductComponents(@Param('id') id: number) {
try {
- let data = await this.productService.getCategoryList(
+ const data = await this.productService.getProductComponents(id);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+
+ // 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
+ @ApiOkResponse()
+ @Post('/:id/components/auto')
+ async autoBindComponents(@Param('id') id: number) {
+ try {
+ const data = await this.productService.autoBindComponentsBySku(id);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+
+ // 获取所有 WordPress 商品
+ @ApiOkResponse({ description: '获取所有 WordPress 商品' })
+ @Get('/wp-products')
+ async getWpProducts() {
+ try {
+ const data = await this.productService.getWpProducts();
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+
+
+ // 通用属性接口:分页列表
+ @ApiOkResponse()
+ @Get('/attribute')
+ async getAttributeList(
+ @Query('dictName') dictName: string,
+ @Query('current') current = 1,
+ @Query('pageSize') pageSize = 10,
+ @Query('name') name?: string
+ ) {
+ try {
+ const data = await this.productService.getAttributeList(
+ dictName,
{ current, pageSize },
name
);
@@ -170,92 +289,138 @@ export class ProductController {
}
}
+ // 通用属性接口:全部列表
@ApiOkResponse()
- @Get('/categorieAll')
- async getCategorieAll() {
+ @Get('/attributeAll')
+ async getAttributeAll(@Query('dictName') dictName: string) {
try {
- let data = await this.productService.getCategoryAll();
+ const data = await this.productService.getAttributeAll(dictName);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @ApiOkResponse({
- type: ProductCatRes,
- })
- @Post('/category')
- async createCategory(@Body() categoryData: CreateCategoryDTO) {
- try {
- const hasCategory = await this.productService.hasCategory(
- categoryData.name
- );
- if (hasCategory) {
- return errorResponse('分类已存在');
- }
- let data = await this.productService.createCategory(categoryData);
- return successResponse(data);
- } catch (error) {
- return errorResponse(error?.message || error);
- }
- }
-
- @ApiOkResponse({
- type: ProductCatRes,
- })
- @Put('/category/:id')
- async updateCategory(
- @Param('id') id: number,
- @Body() categoryData: UpdateCategoryDTO
+ // 通用属性接口:创建
+ @ApiOkResponse()
+ @Post('/attribute')
+ async createAttribute(
+ @Query('dictName') dictName: string,
+ @Body() body: { title: string; name: string }
) {
try {
- const hasCategory = await this.productService.hasCategory(
- categoryData.name
- );
- if (hasCategory) {
- return errorResponse('分类已存在');
+ // 调用 getOrCreateAttribute 方法,如果不存在则创建,如果存在则返回
+ const data = await this.productService.getOrCreateAttribute(dictName, body.title, body.name);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 通用属性接口:更新
+ @ApiOkResponse()
+ @Put('/attribute/:id')
+ async updateAttribute(
+ @Param('id') id: number,
+ @Query('dictName') dictName: string,
+ @Body() body: { title?: string; name?: string }
+ ) {
+ try {
+ if (body?.name) {
+ const hasItem = await this.productService.hasAttribute(
+ dictName,
+ body.name,
+ id
+ );
+ if (hasItem) return errorResponse('字典项已存在');
}
- const data = this.productService.updateCategory(id, categoryData);
+ const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @ApiOkResponse({
- type: BooleanRes,
- })
- @Del('/category/:id')
- async deleteCategory(@Param('id') id: number) {
+ // 通用属性接口:删除
+ @ApiOkResponse({ type: BooleanRes })
+ @Del('/attribute/:id')
+ async deleteAttribute(@Param('id') id: number) {
try {
- const hasProducts = await this.productService.hasProductsInCategory(id);
- if (hasProducts) throw new Error('该分类下有商品,无法删除');
- const data = await this.productService.deleteCategory(id);
- return successResponse(data);
+ await this.productService.deleteAttribute(id);
+ return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @Post('/batchSetSku')
- @ApiOkResponse({
- description: '批量设置 sku 的响应结果',
- type: BooleanRes,
- })
- async batchSetSku(@Body() body: BatchSetSkuDTO) {
+ // 兼容旧接口:品牌
+ @ApiOkResponse()
+ @Get('/brandAll')
+ async compatBrandAll() {
try {
- const result = await this.productService.batchSetSku(body.skus);
- return successResponse(result, '批量设置 sku 成功');
+ const data = await this.productService.getAttributeAll('brand'); // 返回所有品牌字典项
+ return successResponse(data);
} catch (error) {
- return errorResponse(error.message, 400);
+ return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
- @Get('/flavorsAll')
- async getFlavorsAll() {
+ @Get('/brands')
+ async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
- let data = await this.productService.getFlavorsAll();
+ const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 分页品牌列表
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Post('/brand')
+ async compatCreateBrand(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
+ try {
+ const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
+ if (has) return errorResponse('品牌已存在');
+ const data = await this.productService.createAttribute('brand', body); // 创建品牌字典项
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Put('/brand/:id')
+ async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
+ try {
+ if (body?.name) {
+ const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
+ if (has) return errorResponse('品牌已存在');
+ }
+ const data = await this.productService.updateAttribute(id, body); // 更新品牌字典项
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse({ type: BooleanRes })
+ @Del('/brand/:id')
+ async compatDeleteBrand(@Param('id') id: number) {
+ try {
+ await this.productService.deleteAttribute(id); // 删除品牌字典项
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 兼容旧接口:口味
+ @ApiOkResponse()
+ @Get('/flavorsAll')
+ async compatFlavorsAll() {
+ try {
+ const data = await this.productService.getAttributeAll('flavor');
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@@ -264,13 +429,9 @@ export class ProductController {
@ApiOkResponse()
@Get('/flavors')
- async getFlavors(@Query() query: QueryFlavorsDTO) {
- const { current = 1, pageSize = 10, name } = query;
+ async compatFlavors(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
- let data = await this.productService.getFlavorsList(
- { current, pageSize },
- name
- );
+ const data = await this.productService.getAttributeList('flavor', { current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@@ -279,13 +440,11 @@ export class ProductController {
@ApiOkResponse()
@Post('/flavors')
- async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
+ async compatCreateFlavors(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
try {
- const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
- if (hasFlavors) {
- return errorResponse('分类已存在');
- }
- let data = await this.productService.createFlavors(flavorsData);
+ const has = await this.productService.hasAttribute('flavor', body.name);
+ if (has) return errorResponse('口味已存在');
+ const data = await this.productService.createAttribute('flavor', body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@@ -294,42 +453,36 @@ export class ProductController {
@ApiOkResponse()
@Put('/flavors/:id')
- async updateFlavors(
- @Param('id') id: number,
- @Body() flavorsData: UpdateFlavorsDTO
- ) {
+ async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
try {
- const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
- if (hasFlavors) {
- return errorResponse('分类已存在');
+ if (body?.name) {
+ const has = await this.productService.hasAttribute('flavor', body.name, id);
+ if (has) return errorResponse('口味已存在');
}
- const data = this.productService.updateFlavors(id, flavorsData);
+ const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @ApiOkResponse({
- type: BooleanRes,
- })
+ @ApiOkResponse({ type: BooleanRes })
@Del('/flavors/:id')
- async deleteFlavors(@Param('id') id: number) {
+ async compatDeleteFlavors(@Param('id') id: number) {
try {
- const hasProducts = await this.productService.hasProductsInFlavors(id);
- if (hasProducts) throw new Error('该分类下有商品,无法删除');
- const data = await this.productService.deleteFlavors(id);
- return successResponse(data);
+ await this.productService.deleteAttribute(id);
+ return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
+ // 兼容旧接口:规格
@ApiOkResponse()
@Get('/strengthAll')
- async getStrengthAll() {
+ async compatStrengthAll() {
try {
- let data = await this.productService.getStrengthAll();
+ const data = await this.productService.getAttributeAll('strength');
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@@ -338,13 +491,9 @@ export class ProductController {
@ApiOkResponse()
@Get('/strength')
- async getStrength(@Query() query: QueryStrengthDTO) {
- const { current = 1, pageSize = 10, name } = query;
+ async compatStrength(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
- let data = await this.productService.getStrengthList(
- { current, pageSize },
- name
- );
+ const data = await this.productService.getAttributeList('strength', { current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@@ -353,15 +502,11 @@ export class ProductController {
@ApiOkResponse()
@Post('/strength')
- async createStrength(@Body() strengthData: CreateStrengthDTO) {
+ async compatCreateStrength(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
try {
- const hasStrength = await this.productService.hasStrength(
- strengthData.name
- );
- if (hasStrength) {
- return errorResponse('分类已存在');
- }
- let data = await this.productService.createStrength(strengthData);
+ const has = await this.productService.hasAttribute('strength', body.name);
+ if (has) return errorResponse('规格已存在');
+ const data = await this.productService.createAttribute('strength', body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@@ -370,33 +515,182 @@ export class ProductController {
@ApiOkResponse()
@Put('/strength/:id')
- async updateStrength(
- @Param('id') id: number,
- @Body() strengthData: UpdateStrengthDTO
- ) {
+ async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
try {
- const hasStrength = await this.productService.hasStrength(
- strengthData.name
- );
- if (hasStrength) {
- return errorResponse('分类已存在');
+ if (body?.name) {
+ const has = await this.productService.hasAttribute('strength', body.name, id);
+ if (has) return errorResponse('规格已存在');
}
- const data = this.productService.updateStrength(id, strengthData);
+ const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
- @ApiOkResponse({
- type: BooleanRes,
- })
+ @ApiOkResponse({ type: BooleanRes })
@Del('/strength/:id')
- async deleteStrength(@Param('id') id: number) {
+ async compatDeleteStrength(@Param('id') id: number) {
try {
- const hasProducts = await this.productService.hasProductsInStrength(id);
- if (hasProducts) throw new Error('该分类下有商品,无法删除');
- const data = await this.productService.deleteStrength(id);
+ await this.productService.deleteAttribute(id);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 兼容旧接口:尺寸
+ @ApiOkResponse()
+ @Get('/sizeAll')
+ async compatSizeAll() {
+ try {
+ const data = await this.productService.getAttributeAll('size');
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Get('/size')
+ async compatSize(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
+ try {
+ const data = await this.productService.getAttributeList('size', { current, pageSize }, name);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Post('/size')
+ async compatCreateSize(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
+ try {
+ const has = await this.productService.hasAttribute('size', body.name);
+ if (has) return errorResponse('尺寸已存在');
+ const data = await this.productService.createAttribute('size', body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse()
+ @Put('/size/:id')
+ async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
+ try {
+ if (body?.name) {
+ const has = await this.productService.hasAttribute('size', body.name, id);
+ if (has) return errorResponse('尺寸已存在');
+ }
+ const data = await this.productService.updateAttribute(id, body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ @ApiOkResponse({ type: BooleanRes })
+ @Del('/size/:id')
+ async compatDeleteSize(@Param('id') id: number) {
+ try {
+ await this.productService.deleteAttribute(id);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 获取所有分类
+ @ApiOkResponse({ description: '获取所有分类' })
+ @Get('/categories/all')
+ async getCategoriesAll() {
+ try {
+ const data = await this.productService.getCategoriesAll();
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 获取分类下的属性配置
+ @ApiOkResponse({ description: '获取分类下的属性配置' })
+ @Get('/category/:id/attributes')
+ async getCategoryAttributes(@Param('id') id: number) {
+ try {
+ const data = await this.productService.getCategoryAttributes(id);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 创建分类
+ @ApiOkResponse({ description: '创建分类' })
+ @Post('/category')
+ async createCategory(@Body() body: any) {
+ try {
+ const data = await this.productService.createCategory(body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 更新分类
+ @ApiOkResponse({ description: '更新分类' })
+ @Put('/category/:id')
+ async updateCategory(@Param('id') id: number, @Body() body: any) {
+ try {
+ const data = await this.productService.updateCategory(id, body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 删除分类
+ @ApiOkResponse({ description: '删除分类' })
+ @Del('/category/:id')
+ async deleteCategory(@Param('id') id: number) {
+ try {
+ await this.productService.deleteCategory(id);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 创建分类属性
+ @ApiOkResponse({ description: '创建分类属性' })
+ @Post('/category/attribute')
+ async createCategoryAttribute(@Body() body: { categoryId: number; dictId: number }) {
+ try {
+ const data = await this.productService.createCategoryAttribute(body);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 删除分类属性
+ @ApiOkResponse({ description: '删除分类属性' })
+ @Del('/category/attribute/:id')
+ async deleteCategoryAttribute(@Param('id') id: number) {
+ try {
+ await this.productService.deleteCategoryAttribute(id);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error?.message || error);
+ }
+ }
+
+ // 同步库存 SKU 到产品单品
+ @ApiOkResponse({ description: '同步库存 SKU 到产品单品' })
+ @Post('/sync-stock')
+ async syncStockToProduct() {
+ try {
+ const data = await this.productService.syncStockToProduct();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts
new file mode 100644
index 0000000..0518cba
--- /dev/null
+++ b/src/controller/site-api.controller.ts
@@ -0,0 +1,1423 @@
+import { Controller, Get, Inject, Param, Query, Body, Post, Put, Del } from '@midwayjs/core';
+import { ApiOkResponse, ApiBody } from '@midwayjs/swagger';
+import {
+ UploadMediaDTO,
+ UnifiedMediaPaginationDTO,
+ UnifiedOrderDTO,
+ UnifiedOrderPaginationDTO,
+ UnifiedProductDTO,
+ UnifiedProductPaginationDTO,
+ UnifiedSearchParamsDTO,
+ UnifiedSubscriptionPaginationDTO,
+ UnifiedCustomerDTO,
+ UnifiedCustomerPaginationDTO,
+ UnifiedReviewPaginationDTO,
+ UnifiedReviewDTO,
+ CreateReviewDTO,
+ UpdateReviewDTO,
+ UnifiedWebhookDTO,
+ CreateWebhookDTO,
+ UpdateWebhookDTO,
+ UnifiedPaginationDTO,
+ ShipOrderDTO,
+ CancelShipOrderDTO,
+ BatchShipOrdersDTO,
+} from '../dto/site-api.dto';
+import { SiteApiService } from '../service/site-api.service';
+import { errorResponse, successResponse } from '../utils/response.util';
+import { ILogger } from '@midwayjs/core';
+
+
+@Controller('/site-api')
+export class SiteApiController {
+ @Inject()
+ siteApiService: SiteApiService;
+
+ @Inject()
+ logger: ILogger;
+
+
+
+
+
+ @Post('/:siteId/media/create')
+ @ApiOkResponse({ type: UnifiedMediaPaginationDTO })
+ @ApiBody({ type: UploadMediaDTO })
+ async createMedia(
+ @Param('siteId') siteId: number,
+ @Body() body: UploadMediaDTO,
+ ) {
+ this.logger.debug(`[Site API] 上传媒体文件开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createMedia(body.file);
+ this.logger.debug(`[Site API] 上传媒体文件成功, siteId: ${siteId}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 上传媒体文件失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+
+ @Get('/:siteId/reviews')
+ @ApiOkResponse({ type: UnifiedReviewPaginationDTO })
+ async getReviews(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.debug(`[Site API] 获取评论列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getReviews(query);
+ this.logger.debug(`[Site API] 获取评论列表成功, siteId: ${siteId}, 共获取到 ${data.total} 条评论`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取评论列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/reviews')
+ @ApiOkResponse({ type: UnifiedReviewDTO })
+ async createReview(
+ @Param('siteId') siteId: number,
+ @Body() body: CreateReviewDTO
+ ) {
+ this.logger.debug(`[Site API] 创建评论开始, siteId: ${siteId}, body: ${JSON.stringify(body)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createReview(body);
+ this.logger.debug(`[Site API] 创建评论成功, siteId: ${siteId}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 创建评论失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/reviews/:id')
+ @ApiOkResponse({ type: UnifiedReviewDTO })
+ async updateReview(
+ @Param('siteId') siteId: number,
+ @Param('id') id: number,
+ @Body() body: UpdateReviewDTO
+ ) {
+ this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.updateReview(id, body);
+ this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 更新评论失败, siteId: ${siteId}, id: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:siteId/reviews/:id')
+ @ApiOkResponse({ type: Boolean })
+ async deleteReview(
+ @Param('siteId') siteId: number,
+ @Param('id') id: number
+ ) {
+ this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.deleteReview(id);
+ this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 删除评论失败, siteId: ${siteId}, id: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/webhooks')
+ @ApiOkResponse({ type: UnifiedPaginationDTO })
+ async getWebhooks(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.debug(`[Site API] 获取webhooks列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getWebhooks(query);
+ this.logger.debug(`[Site API] 获取webhooks列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个webhooks`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取webhooks列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/webhooks/:id')
+ @ApiOkResponse({ type: UnifiedWebhookDTO })
+ async getWebhook(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getWebhook(id);
+ this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取单个webhook失败, siteId: ${siteId}, id: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/webhooks')
+ @ApiOkResponse({ type: UnifiedWebhookDTO })
+ @ApiBody({ type: CreateWebhookDTO })
+ async createWebhook(
+ @Param('siteId') siteId: number,
+ @Body() body: CreateWebhookDTO
+ ) {
+ this.logger.debug(`[Site API] 创建webhook开始, siteId: ${siteId}, body: ${JSON.stringify(body)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createWebhook(body);
+ this.logger.debug(`[Site API] 创建webhook成功, siteId: ${siteId}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 创建webhook失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/webhooks/:id')
+ @ApiOkResponse({ type: UnifiedWebhookDTO })
+ @ApiBody({ type: UpdateWebhookDTO })
+ async updateWebhook(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: UpdateWebhookDTO
+ ) {
+ this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.updateWebhook(id, body);
+ this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 更新webhook失败, siteId: ${siteId}, id: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:siteId/webhooks/:id')
+ @ApiOkResponse({ type: Boolean })
+ async deleteWebhook(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.deleteWebhook(id);
+ this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 删除webhook失败, siteId: ${siteId}, id: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/products')
+ @ApiOkResponse({ type: UnifiedProductPaginationDTO })
+ async getProducts(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.debug(`[Site API] 获取产品列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getProducts(query);
+
+ // 如果包含ERP产品信息,则增强商品数据
+ if (data && data.items && data.items.length > 0) {
+ const enrichedItems = await this.siteApiService.enrichSiteProductsWithErpInfo(siteId, data.items);
+ data.items = enrichedItems;
+ }
+
+ this.logger.debug(`[Site API] 获取产品列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个产品`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取产品列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/products/export')
+ async exportProducts(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const perPage = (query.per_page) || 100;
+ let page = 1;
+ const all: any[] = [];
+ while (true) {
+ const data = await adapter.getProducts({ ...query, page, per_page: 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);
+ }
+ }
+
+ // 平台特性:产品导出(特殊CSV,走平台服务)
+ @Get('/:siteId/links')
+ async getLinks(
+ @Param('siteId') siteId: number
+ ) {
+ this.logger.debug(`[Site API] 获取站点链接列表开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getLinks();
+ this.logger.debug(`[Site API] 获取站点链接列表成功, siteId: ${siteId}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取站点链接列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/products/export-special')
+ async exportProductsSpecial(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ try {
+ const site = await this.siteApiService.siteService.get(siteId, true);
+ if (site.type === 'woocommerce') {
+ const page = query.page || 1;
+ const perPage = (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 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 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');
+ return successResponse({ csv });
+ }
+ throw new Error('Unsupported site type for special export');
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/products/:id')
+ @ApiOkResponse({ type: UnifiedProductDTO })
+ async getProduct(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getProduct(id);
+
+ // 如果获取到商品数据,则增强ERP产品信息
+ if (data) {
+ const enrichedData = await this.siteApiService.enrichSiteProductWithErpInfo(siteId, data);
+ this.logger.info(`[Site API] 获取单个产品成功, siteId: ${siteId}, productId: ${id}`);
+ return successResponse(enrichedData);
+ }
+
+ this.logger.info(`[Site API] 获取单个产品成功, siteId: ${siteId}, productId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取单个产品失败, siteId: ${siteId}, productId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/products')
+ @ApiOkResponse({ type: UnifiedProductDTO })
+ async createProduct(
+ @Param('siteId') siteId: number,
+ @Body() body: UnifiedProductDTO
+ ) {
+ this.logger.info(`[Site API] 创建产品开始, siteId: ${siteId}, 产品名称: ${body.name}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createProduct(body);
+ this.logger.info(`[Site API] 创建产品成功, siteId: ${siteId}, 产品ID: ${data.id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 创建产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/products/import')
+ @ApiOkResponse({ type: Object })
+ async importProducts(
+ @Param('siteId') siteId: number,
+ @Body() body: { items?: any[]; csv?: string }
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ let items = body.items || [];
+ if (!items.length && body.csv) {
+ const lines = body.csv.split(/\r?\n/).filter(Boolean);
+ const header = lines.shift()?.split(',') || [];
+ items = lines.map((line) => {
+ const cols = line.split(',');
+ const obj: any = {};
+ header.forEach((h, i) => (obj[h] = cols[i]));
+ return obj;
+ });
+ }
+ const created: any[] = [];
+ const failed: any[] = [];
+ for (const item of items) {
+ try {
+ const data = await adapter.createProduct(item);
+ created.push(data);
+ } catch (e) {
+ failed.push({ item, error: (e as any).message });
+ }
+ }
+ return successResponse({ created, failed });
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ // 平台特性:产品导入(特殊CSV,走平台服务)
+ @Post('/:siteId/products/import-special')
+ @ApiOkResponse({ type: Object })
+ async importProductsSpecial(
+ @Param('siteId') siteId: number,
+ @Body() body: { csv?: string; items?: any[] }
+ ) {
+ try {
+ const site = await this.siteApiService.siteService.get(siteId, true);
+ const csvText = body.csv || '';
+ const items = body.items || [];
+ const created: any[] = [];
+ const failed: any[] = [];
+ if (site.type === 'woocommerce') {
+ // 解析 CSV 为对象数组(若传入 items 则优先 items)
+ let payloads = items;
+ if (!payloads.length && csvText) {
+ const lines = csvText.split(/\r?\n/).filter(Boolean);
+ const header = lines.shift()?.split(',') || [];
+ payloads = lines.map((line) => {
+ const cols = line.split(',');
+ const obj: any = {};
+ header.forEach((h, i) => (obj[h] = cols[i]));
+ return obj;
+ });
+ }
+ for (const item of payloads) {
+ try {
+ const res = await this.siteApiService.wpService.createProduct(site, item);
+ created.push(res);
+ } catch (e) {
+ failed.push({ item, error: (e as any).message });
+ }
+ }
+ return successResponse({ created, failed });
+ }
+ if (site.type === 'shopyy') {
+ throw new Error('ShopYY 暂不支持特殊CSV导入');
+ }
+ throw new Error('Unsupported site type for special import');
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/products/:id')
+ @ApiOkResponse({ type: UnifiedProductDTO })
+ async updateProduct(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: UnifiedProductDTO
+ ) {
+ this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.updateProduct(id, body);
+ this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 更新产品失败, siteId: ${siteId}, productId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/products/:productId/variations/:variationId')
+ @ApiOkResponse({ type: Object })
+ async updateVariation(
+ @Param('siteId') siteId: number,
+ @Param('productId') productId: string,
+ @Param('variationId') variationId: string,
+ @Body() body: any
+ ) {
+ this.logger.info(`[Site API] 更新产品变体开始, siteId: ${siteId}, productId: ${productId}, variationId: ${variationId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.updateVariation(productId, variationId, body);
+ this.logger.info(`[Site API] 更新产品变体成功, siteId: ${siteId}, productId: ${productId}, variationId: ${variationId}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 更新产品变体失败, siteId: ${siteId}, productId: ${productId}, variationId: ${variationId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:siteId/products/:id')
+ @ApiOkResponse({ type: Boolean })
+ async deleteProduct(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const success = await adapter.deleteProduct(id);
+ this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`);
+ return successResponse(success);
+ } catch (error) {
+ this.logger.error(`[Site API] 删除产品失败, siteId: ${siteId}, productId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/products/batch')
+ @ApiOkResponse({ type: Object })
+ async batchProducts(
+ @Param('siteId') siteId: number,
+ @Body() body: { create?: any[]; update?: any[]; delete?: Array }
+ ) {
+ this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ if (adapter.batchProcessProducts) {
+ const res = await adapter.batchProcessProducts(body);
+ this.logger.info(`[Site API] 批量处理产品成功, siteId: ${siteId}`);
+ return successResponse(res);
+ }
+ const created: any[] = [];
+ const updated: any[] = [];
+ const deleted: Array = [];
+ const failed: any[] = [];
+ if (body.create?.length) {
+ for (const item of body.create) {
+ try {
+ const data = await adapter.createProduct(item);
+ created.push(data);
+ } catch (e) {
+ failed.push({ action: 'create', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.update?.length) {
+ for (const item of body.update) {
+ try {
+ const id = item.id;
+ const data = await adapter.updateProduct(id, item);
+ updated.push(data);
+ } catch (e) {
+ failed.push({ action: 'update', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.delete?.length) {
+ for (const id of body.delete) {
+ try {
+ const ok = await adapter.deleteProduct(id);
+ if (ok) deleted.push(id);
+ else failed.push({ action: 'delete', id, error: 'delete failed' });
+ } catch (e) {
+ failed.push({ action: 'delete', id, error: (e as any).message });
+ }
+ }
+ }
+ this.logger.info(`[Site API] 批量处理产品完成, siteId: ${siteId}`);
+ return successResponse({ created, updated, deleted, failed });
+ } catch (error) {
+ this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/orders')
+ @ApiOkResponse({ type: UnifiedOrderPaginationDTO })
+ async getOrders(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.info(`[Site API] 获取订单列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ 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) {
+ this.logger.error(`[Site API] 获取订单列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @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,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const perPage = (query.per_page) || 100;
+ let page = 1;
+ const all: any[] = [];
+ while (true) {
+ const data = await adapter.getOrders({ ...query, page, per_page: 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);
+ }
+ }
+
+ @Get('/:siteId/orders/:id')
+ @ApiOkResponse({ type: UnifiedOrderDTO })
+ async getOrder(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getOrder(id);
+ this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取单个订单失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders')
+ @ApiOkResponse({ type: UnifiedOrderDTO })
+ async createOrder(
+ @Param('siteId') siteId: number,
+ @Body() body: any
+ ) {
+ this.logger.info(`[Site API] 创建订单开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createOrder(body);
+ this.logger.info(`[Site API] 创建订单成功, siteId: ${siteId}, orderId: ${data.id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 创建订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders/import')
+ @ApiOkResponse({ type: Object })
+ async importOrders(
+ @Param('siteId') siteId: number,
+ @Body() body: { items?: any[]; csv?: string }
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ let items = body.items || [];
+ if (!items.length && body.csv) {
+ const lines = body.csv.split(/\r?\n/).filter(Boolean);
+ const header = lines.shift()?.split(',') || [];
+ items = lines.map((line) => {
+ const cols = line.split(',');
+ const obj: any = {};
+ header.forEach((h, i) => (obj[h] = cols[i]));
+ return obj;
+ });
+ }
+ const created: any[] = [];
+ const failed: any[] = [];
+ for (const item of items) {
+ try {
+ const data = await adapter.createOrder(item);
+ created.push(data);
+ } catch (e) {
+ failed.push({ item, error: (e as any).message });
+ }
+ }
+ return successResponse({ created, failed });
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/orders/:id')
+ @ApiOkResponse({ type: Boolean })
+ async updateOrder(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: any
+ ) {
+ this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const ok = await adapter.updateOrder(id, body);
+ this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`);
+ return successResponse(ok);
+ } catch (error) {
+ this.logger.error(`[Site API] 更新订单失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:siteId/orders/:id')
+ @ApiOkResponse({ type: Boolean })
+ async deleteOrder(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const ok = await adapter.deleteOrder(id);
+ this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`);
+ return successResponse(ok);
+ } catch (error) {
+ this.logger.error(`[Site API] 删除订单失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders/batch')
+ @ApiOkResponse({ type: Object })
+ async batchOrders(
+ @Param('siteId') siteId: number,
+ @Body() body: { create?: any[]; update?: any[]; delete?: Array }
+ ) {
+ this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const created: any[] = [];
+ const updated: any[] = [];
+ const deleted: Array = [];
+ const failed: any[] = [];
+ if (body.create?.length) {
+ for (const item of body.create) {
+ try {
+ const data = await adapter.createOrder(item);
+ created.push(data);
+ } catch (e) {
+ failed.push({ action: 'create', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.update?.length) {
+ for (const item of body.update) {
+ try {
+ const id = item.id;
+ const ok = await adapter.updateOrder(id, item);
+ if (ok) updated.push(item);
+ else failed.push({ action: 'update', item, error: 'update failed' });
+ } catch (e) {
+ failed.push({ action: 'update', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.delete?.length) {
+ for (const id of body.delete) {
+ try {
+ const ok = await adapter.deleteOrder(id);
+ if (ok) deleted.push(id);
+ else failed.push({ action: 'delete', id, error: 'delete failed' });
+ } catch (e) {
+ failed.push({ action: 'delete', id, error: (e as any).message });
+ }
+ }
+ }
+ this.logger.info(`[Site API] 批量处理订单完成, siteId: ${siteId}`);
+ return successResponse({ created, updated, deleted, failed });
+ } catch (error) {
+ this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/orders/:id/notes')
+ @ApiOkResponse({ type: Object })
+ async getOrderNotes(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 获取订单备注开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getOrderNotes(id);
+ this.logger.info(`[Site API] 获取订单备注成功, siteId: ${siteId}, orderId: ${id}, 共获取到 ${data.length} 条备注`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取订单备注失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders/:id/notes')
+ @ApiOkResponse({ type: Object })
+ async createOrderNote(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: any
+ ) {
+ this.logger.info(`[Site API] 创建订单备注开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createOrderNote(id, body);
+ this.logger.info(`[Site API] 创建订单备注成功, siteId: ${siteId}, orderId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 创建订单备注失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders/:id/ship')
+ @ApiOkResponse({ type: Object })
+ async shipOrder(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: ShipOrderDTO
+ ) {
+ this.logger.info(`[Site API] 订单发货开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.shipOrder(id, body);
+ this.logger.info(`[Site API] 订单发货成功, siteId: ${siteId}, orderId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 订单发货失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders/:id/cancel-ship')
+ @ApiOkResponse({ type: Object })
+ async cancelShipOrder(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: CancelShipOrderDTO
+ ) {
+ this.logger.info(`[Site API] 取消订单发货开始, siteId: ${siteId}, orderId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.cancelShipOrder(id, body);
+ this.logger.info(`[Site API] 取消订单发货成功, siteId: ${siteId}, orderId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 取消订单发货失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/orders/batch-ship')
+ @ApiOkResponse({ type: Object })
+ async batchShipOrders(
+ @Param('siteId') siteId: number,
+ @Body() body: BatchShipOrdersDTO
+ ) {
+ this.logger.info(`[Site API] 批量订单发货开始, siteId: ${siteId}, 订单数量: ${body.orders.length}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const results = await Promise.allSettled(
+ body.orders.map(order =>
+ adapter.shipOrder(order.order_id, {
+ tracking_number: order.tracking_number,
+ shipping_provider: order.shipping_provider,
+ shipping_method: order.shipping_method,
+ items: order.items,
+ }).catch(error => ({
+ order_id: order.order_id,
+ success: false,
+ error: error.message
+ }))
+ )
+ );
+
+ const successful = results
+ .filter(result => result.status === 'fulfilled')
+ .map(result => (result as PromiseFulfilledResult).value);
+
+ const failed = results
+ .filter(result => result.status === 'rejected')
+ .map(result => (result as PromiseRejectedResult).reason);
+
+ this.logger.info(`[Site API] 批量订单发货完成, siteId: ${siteId}, 成功: ${successful.length}, 失败: ${failed.length}`);
+ return successResponse({
+ successful: successful.length,
+ failed: failed.length,
+ results: {
+ successful,
+ failed
+ }
+ });
+ } catch (error) {
+ this.logger.error(`[Site API] 批量订单发货失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/subscriptions')
+ @ApiOkResponse({ type: UnifiedSubscriptionPaginationDTO })
+ async getSubscriptions(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.info(`[Site API] 获取订阅列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getSubscriptions(query);
+ this.logger.info(`[Site API] 获取订阅列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订阅`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取订阅列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/subscriptions/export')
+ async exportSubscriptions(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const perPage = (query.per_page) || 100;
+ let page = 1;
+ const all: any[] = [];
+ while (true) {
+ const data = await adapter.getSubscriptions({ ...query, page, per_page: 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 = 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) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/media')
+ @ApiOkResponse({ type: UnifiedMediaPaginationDTO })
+ async getMedia(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.info(`[Site API] 获取媒体列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getMedia(query);
+ this.logger.info(`[Site API] 获取媒体列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个媒体`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取媒体列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/media/export')
+ async exportMedia(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const perPage = (query.per_page) || 100;
+ let page = 1;
+ const all: any[] = [];
+ while (true) {
+ const data = await adapter.getMedia({ ...query, page, per_page: 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 = 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) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:siteId/media/:id')
+ @ApiOkResponse({ type: Boolean })
+ async deleteMedia(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 删除媒体开始, siteId: ${siteId}, mediaId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const api: any = adapter as any;
+ if (api.deleteMedia) {
+ const success = await api.deleteMedia(id);
+ this.logger.info(`[Site API] 删除媒体成功, siteId: ${siteId}, mediaId: ${id}`);
+ return successResponse(success);
+ }
+ throw new Error('Media delete not supported');
+ } catch (error) {
+ this.logger.error(`[Site API] 删除媒体失败, siteId: ${siteId}, mediaId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/media/:id')
+ @ApiOkResponse({ type: Object })
+ async updateMedia(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: any
+ ) {
+ this.logger.info(`[Site API] 更新媒体开始, siteId: ${siteId}, mediaId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const api: any = adapter as any;
+ if (api.updateMedia) {
+ const res = await api.updateMedia(id, body);
+ this.logger.info(`[Site API] 更新媒体成功, siteId: ${siteId}, mediaId: ${id}`);
+ return successResponse(res);
+ }
+ throw new Error('Media update not supported');
+ } catch (error) {
+ this.logger.error(`[Site API] 更新媒体失败, siteId: ${siteId}, mediaId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/media/batch')
+ @ApiOkResponse({ type: Object })
+ async batchMedia(
+ @Param('siteId') siteId: number,
+ @Body() body: { update?: any[]; delete?: Array }
+ ) {
+ this.logger.info(`[Site API] 批量处理媒体开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const updated: any[] = [];
+ const deleted: Array = [];
+ const failed: any[] = [];
+ const api: any = adapter as any;
+ if (body.update?.length) {
+ for (const item of body.update) {
+ try {
+ if (!api.updateMedia) throw new Error('Media update not supported');
+ const res = await api.updateMedia(item.id, item);
+ updated.push(res);
+ } catch (e) {
+ failed.push({ action: 'update', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.delete?.length) {
+ for (const id of body.delete) {
+ try {
+ if (!api.deleteMedia) throw new Error('Media delete not supported');
+ const ok = await api.deleteMedia(id);
+ if (ok) deleted.push(id);
+ else failed.push({ action: 'delete', id, error: 'delete failed' });
+ } catch (e) {
+ failed.push({ action: 'delete', id, error: (e as any).message });
+ }
+ }
+ }
+ this.logger.info(`[Site API] 批量处理媒体完成, siteId: ${siteId}`);
+ return successResponse({ updated, deleted, failed });
+ } catch (error) {
+ this.logger.error(`[Site API] 批量处理媒体失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/media/convert-webp')
+ @ApiOkResponse({ type: Object })
+ async convertMediaToWebp(
+ @Param('siteId') siteId: number,
+ @Body() body: { ids: Array }
+ ) {
+ this.logger.info(`[Site API] 批量转换媒体为 webp 开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const api: any = adapter as any;
+ // 条件判断 如果未提供 ids 列表则抛出错误
+ if (!body?.ids || body.ids.length === 0) {
+ throw new Error('未提供需要转换的媒体ID列表');
+ }
+ if (!api.convertMediaToWebp) {
+ throw new Error('当前站点不支持媒体转换');
+ }
+ const res = await api.convertMediaToWebp(body.ids);
+ this.logger.info(`[Site API] 批量转换媒体为 webp 成功, siteId: ${siteId}`);
+ return successResponse(res);
+ } catch (error) {
+ this.logger.error(`[Site API] 批量转换媒体为 webp 失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/customers')
+ @ApiOkResponse({ type: UnifiedCustomerPaginationDTO })
+ async getCustomers(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ this.logger.info(`[Site API] 获取客户列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getCustomers(query);
+ this.logger.info(`[Site API] 获取客户列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个客户`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取客户列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/customers/export')
+ async exportCustomers(
+ @Param('siteId') siteId: number,
+ @Query() query: UnifiedSearchParamsDTO
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const perPage = (query.per_page) || 100;
+ let page = 1;
+ const all: any[] = [];
+ while (true) {
+ const data = await adapter.getCustomers({ ...query, page, per_page: 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) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Get('/:siteId/customers/:id')
+ @ApiOkResponse({ type: UnifiedCustomerDTO })
+ async getCustomer(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.getCustomer(id);
+ this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 获取单个客户失败, siteId: ${siteId}, customerId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/customers')
+ @ApiOkResponse({ type: UnifiedCustomerDTO })
+ async createCustomer(
+ @Param('siteId') siteId: number,
+ @Body() body: UnifiedCustomerDTO
+ ) {
+ this.logger.info(`[Site API] 创建客户开始, siteId: ${siteId}, 客户邮箱: ${body.email}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.createCustomer(body);
+ this.logger.info(`[Site API] 创建客户成功, siteId: ${siteId}, customerId: ${data.id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 创建客户失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/customers/import')
+ @ApiOkResponse({ type: Object })
+ async importCustomers(
+ @Param('siteId') siteId: number,
+ @Body() body: { items?: any[]; csv?: string }
+ ) {
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ let items = body.items || [];
+ if (!items.length && body.csv) {
+ const lines = body.csv.split(/\r?\n/).filter(Boolean);
+ const header = lines.shift()?.split(',') || [];
+ items = lines.map((line) => {
+ const cols = line.split(',');
+ const obj: any = {};
+ header.forEach((h, i) => (obj[h] = cols[i]));
+ return obj;
+ });
+ }
+ const created: any[] = [];
+ const failed: any[] = [];
+ for (const item of items) {
+ try {
+ const data = await adapter.createCustomer(item);
+ created.push(data);
+ } catch (e) {
+ failed.push({ item, error: (e as any).message });
+ }
+ }
+ return successResponse({ created, failed });
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ @Put('/:siteId/customers/:id')
+ @ApiOkResponse({ type: UnifiedCustomerDTO })
+ async updateCustomer(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string,
+ @Body() body: UnifiedCustomerDTO
+ ) {
+ this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const data = await adapter.updateCustomer(id, body);
+ this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`);
+ return successResponse(data);
+ } catch (error) {
+ this.logger.error(`[Site API] 更新客户失败, siteId: ${siteId}, customerId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Del('/:siteId/customers/:id')
+ @ApiOkResponse({ type: Boolean })
+ async deleteCustomer(
+ @Param('siteId') siteId: number,
+ @Param('id') id: string
+ ) {
+ this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const success = await adapter.deleteCustomer(id);
+ this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`);
+ return successResponse(success);
+ } catch (error) {
+ this.logger.error(`[Site API] 删除客户失败, siteId: ${siteId}, customerId: ${id}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+ @Post('/:siteId/customers/batch')
+ @ApiOkResponse({ type: Object })
+ async batchCustomers(
+ @Param('siteId') siteId: number,
+ @Body() body: { create?: any[]; update?: any[]; delete?: Array }
+ ) {
+ this.logger.info(`[Site API] 批量处理客户开始, siteId: ${siteId}`);
+ try {
+ const adapter = await this.siteApiService.getAdapter(siteId);
+ const created: any[] = [];
+ const updated: any[] = [];
+ const deleted: Array = [];
+ const failed: any[] = [];
+ if (body.create?.length) {
+ for (const item of body.create) {
+ try {
+ const data = await adapter.createCustomer(item);
+ created.push(data);
+ } catch (e) {
+ failed.push({ action: 'create', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.update?.length) {
+ for (const item of body.update) {
+ try {
+ const id = item.id;
+ const data = await adapter.updateCustomer(id, item);
+ updated.push(data);
+ } catch (e) {
+ failed.push({ action: 'update', item, error: (e as any).message });
+ }
+ }
+ }
+ if (body.delete?.length) {
+ for (const id of body.delete) {
+ try {
+ const ok = await adapter.deleteCustomer(id);
+ if (ok) deleted.push(id);
+ else failed.push({ action: 'delete', id, error: 'delete failed' });
+ } catch (e) {
+ failed.push({ action: 'delete', id, error: (e as any).message });
+ }
+ }
+ }
+ this.logger.info(`[Site API] 批量处理客户完成, siteId: ${siteId}`);
+ return successResponse({ created, updated, deleted, failed });
+ } catch (error) {
+ this.logger.error(`[Site API] 批量处理客户失败, siteId: ${siteId}, 错误信息: ${error.message}`);
+ return errorResponse(error.message);
+ }
+ }
+
+
+}
diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts
index 5c848d5..02cf312 100644
--- a/src/controller/site.controller.ts
+++ b/src/controller/site.controller.ts
@@ -15,7 +15,7 @@ export class SiteController {
async all() {
try {
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
- return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName })));
+ return successResponse(items.map((v: any) => ({ id: v.id, name: v.name })));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
diff --git a/src/controller/stock.controller.ts b/src/controller/stock.controller.ts
index a407df0..1728bda 100644
--- a/src/controller/stock.controller.ts
+++ b/src/controller/stock.controller.ts
@@ -176,9 +176,21 @@ export class StockController {
}
}
+ // 检查某个 SKU 是否有库存(任一仓库数量大于 0)
+ @ApiOkResponse({ type: BooleanRes })
+ @Get('/has/:sku')
+ async hasStock(@Param('sku') sku: string) {
+ try {
+ const data = await this.stockService.hasStockBySku(sku);
+ return successResponse(data);
+ } catch (error) {
+ return errorResponse(error?.message || '查询失败');
+ }
+ }
+
@ApiOkResponse({
type: BooleanRes,
- description: '更新库存(入库、出库、调整)',
+ description: '更新库存(入库,出库,调整)',
})
@Post('/update')
async updateStock(@Body() body: UpdateStockDTO) {
diff --git a/src/controller/subscription.controller.ts b/src/controller/subscription.controller.ts
index 5506063..61059e3 100644
--- a/src/controller/subscription.controller.ts
+++ b/src/controller/subscription.controller.ts
@@ -10,19 +10,19 @@ export class SubscriptionController {
@Inject()
subscriptionService: SubscriptionService;
- // 同步订阅:根据站点 ID 拉取并更新本地订阅数据
+ // 同步订阅:根据站点 ID 拉取并更新本地订阅数据
@ApiOkResponse({ type: BooleanRes })
@Post('/sync/:siteId')
- async sync(@Param('siteId') siteId: string) {
+ async sync(@Param('siteId') siteId: number) {
try {
- await this.subscriptionService.syncSubscriptions(siteId);
- return successResponse(true);
+ const result = await this.subscriptionService.syncSubscriptions(siteId);
+ return successResponse(result);
} catch (error) {
return errorResponse(error?.message || '同步失败');
}
}
- // 订阅列表:分页 + 筛选
+ // 订阅列表:分页 + 筛选
@ApiOkResponse({ type: SubscriptionListRes })
@Get('/list')
async list(@Query() query: QuerySubscriptionDTO) {
diff --git a/src/controller/template.controller.ts b/src/controller/template.controller.ts
new file mode 100644
index 0000000..330ce2b
--- /dev/null
+++ b/src/controller/template.controller.ts
@@ -0,0 +1,168 @@
+import { Inject, Controller, Get, Post, Put, Del, Body, Param, Query } from '@midwayjs/core';
+import { TemplateService } from '../service/template.service';
+import { successResponse, errorResponse } from '../utils/response.util';
+import { CreateTemplateDTO, UpdateTemplateDTO, RenderTemplateDTO } from '../dto/template.dto';
+import { ApiOkResponse, ApiTags } from '@midwayjs/swagger';
+import { Template } from '../entity/template.entity';
+import { BooleanRes } from '../dto/reponse.dto';
+
+/**
+ * @controller TemplateController 模板管理
+ */
+@ApiTags('Template')
+@Controller('/template')
+export class TemplateController {
+ @Inject()
+ templateService: TemplateService;
+
+ /**
+ * @summary 获取模板列表
+ * @description 获取所有可用模板的列表
+ */
+ @ApiOkResponse({ type: [Template], description: '成功获取模板列表' })
+ @Get('/list')
+ async getTemplateList(@Query() params: any) {
+ // 调用服务层获取列表
+ return this.templateService.getTemplateList(params);
+ }
+
+ /**
+ * @summary 根据名称获取模板
+ * @description 通过模板的唯一名称查找特定模板
+ * @param name 模板名称
+ */
+ @ApiOkResponse({ type: Template, description: '成功获取模板' })
+ @Get('/:name')
+ async getTemplateByName(@Param('name') name: string) {
+ try {
+ // 调用服务层获取单个模板
+ const data = await this.templateService.getTemplateByName(name);
+ // 返回成功响应
+ return successResponse(data);
+ } catch (error) {
+ // 返回错误响应
+ return errorResponse(error.message);
+ }
+ }
+
+ /**
+ * @summary 创建新模板
+ * @description 创建一个新的模板,用于后续的字符串生成
+ * @param templateData 模板数据
+ */
+ @ApiOkResponse({ type: Template, description: '成功创建模板' })
+ @Post('/')
+ async createTemplate(@Body() templateData: CreateTemplateDTO) {
+ try {
+ // 调用服务层创建模板
+ const data = await this.templateService.createTemplate(templateData);
+ // 返回成功响应
+ return successResponse(data);
+ } catch (error) {
+ // 返回错误响应
+ return errorResponse(error.message);
+ }
+ }
+
+ /**
+ * @summary 更新现有模板
+ * @description 根据模板 ID 更新一个现有模板的内容
+ * @param id 模板 ID
+ * @param templateData 模板数据
+ */
+ @ApiOkResponse({ type: Template, description: '成功更新模板' })
+ @Put('/:id')
+ async updateTemplate(
+ @Param('id') id: number,
+ @Body() templateData: UpdateTemplateDTO
+ ) {
+ try {
+ // 调用服务层更新模板
+ const data = await this.templateService.updateTemplate(id, templateData);
+ // 返回成功响应
+ return successResponse(data);
+ } catch (error) {
+ // 返回错误响应
+ return errorResponse(error.message);
+ }
+ }
+
+ /**
+ * @summary 删除模板
+ * @description 根据模板 ID 删除一个模板
+ * @param id 模板 ID
+ */
+ @ApiOkResponse({ type: BooleanRes, description: '成功删除模板' })
+ @Del('/:id')
+ async deleteTemplate(@Param('id') id: number) {
+ try {
+ // 调用服务层删除模板
+ const data = await this.templateService.deleteTemplate(id);
+ // 返回成功响应
+ return successResponse(data);
+ } catch (error) {
+ // 返回错误响应
+ return errorResponse(error.message);
+ }
+ }
+
+ /**
+ * @summary 渲染模板
+ * @description 根据模板名称和输入的数据渲染最终字符串
+ * @param name 模板名称
+ * @param data 渲染数据
+ */
+ @ApiOkResponse({ type: String, description: '成功渲染模板' })
+ @Post('/render/:name')
+ async renderTemplate(
+ @Param('name') name: string,
+ @Body() data: Record
+ ) {
+ try {
+ // 调用服务层渲染模板
+ const renderedString = await this.templateService.render(name, data);
+ // 返回成功响应
+ return successResponse(renderedString);
+ } catch (error) {
+ // 返回错误响应
+ return errorResponse(error.message);
+ }
+ }
+
+ /**
+ * @summary 回填缺失的测试数据
+ * @description 扫描数据库中所有模板,为缺失 testData 的记录生成并保存测试数据
+ */
+ @ApiOkResponse({ type: Number, description: '成功回填的数量' })
+ @Post('/backfill-testdata')
+ async backfillTestData() {
+ try {
+ const count = await this.templateService.backfillMissingTestData();
+ return successResponse({ updated: count });
+ } catch (error) {
+ return errorResponse(error.message);
+ }
+ }
+
+ /**
+ * @summary 直接渲染模板内容
+ * @description 直接传入模板内容和数据渲染最终字符串,无需保存模板到数据库
+ * @param renderData 包含模板内容和渲染数据的对象
+ */
+ @ApiOkResponse({ type: String, description: '成功渲染模板' })
+ @Post('/render-direct')
+ async renderTemplateDirect(@Body() renderData: RenderTemplateDTO) {
+ try {
+ // 调用服务层渲染模板内容
+ const renderedString = await this.templateService.renderWithTemplate(
+ renderData.template,
+ renderData.data
+ );
+ // 返回成功响应
+ return successResponse(renderedString);
+ } catch (error) {
+ // 返回错误响应
+ return errorResponse(error.message);
+ }
+ }
+}
diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts
index 662c253..711f1f2 100644
--- a/src/controller/user.controller.ts
+++ b/src/controller/user.controller.ts
@@ -34,16 +34,17 @@ export class UserController {
})
@Post('/logout')
async logout() {
- // 可选:在这里处理服务端缓存的 token 或 session
+ // 可选:在这里处理服务端缓存的 token 或 session
return successResponse(true);
}
@Post('/add')
- async addUser(@Body() body: { username: string; password: string }) {
- const { username, password } = body;
+ async addUser(@Body() body: { username: string; password: string; email?: string; remark?: string }) {
+ const { username, password, email, remark } = body;
try {
- await this.userService.addUser(username, password);
+ // 新增用户 支持邮箱与备注
+ await this.userService.addUser(username, password, remark, email);
return successResponse(true);
} catch (error) {
console.log(error);
@@ -52,21 +53,91 @@ export class UserController {
}
@Get('/list')
- async listUsers(@Query() query: { current: number; pageSize: number }) {
- const { current = 1, pageSize = 10 } = query;
- return successResponse(await this.userService.listUsers(current, pageSize));
+ async listUsers(
+ @Query()
+ query: {
+ current: number;
+ pageSize: number;
+ remark?: string;
+ username?: string;
+ email?: string;
+ isActive?: string;
+ isSuper?: string;
+ isAdmin?: string;
+ sortField?: string;
+ sortOrder?: string;
+ }
+ ) {
+ const { current = 1, pageSize = 10, remark, username, email, isActive, isSuper, isAdmin, sortField, sortOrder } = query;
+ // 将字符串布尔转换为真实布尔
+ const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
+ // 处理排序方向
+ const order = (sortOrder === 'ascend' || sortOrder === 'ASC') ? 'ASC' : 'DESC';
+
+ // 列表移除密码字段
+ const { items, total } = await this.userService.listUsers(
+ current,
+ pageSize,
+ {
+ remark,
+ username,
+ email,
+ isActive: toBool(isActive),
+ isSuper: toBool(isSuper),
+ isAdmin: toBool(isAdmin),
+ },
+ {
+ field: sortField,
+ order,
+ }
+ );
+ const safeItems = (items || []).map((it: any) => {
+ const { password, ...rest } = it || {};
+ return rest;
+ });
+ return successResponse({ items: safeItems, total, current, pageSize });
}
@Post('/toggleActive')
async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
- return this.userService.toggleUserActive(body.userId, body.isActive);
+ try {
+ // 调用服务层更新启用状态
+ const data = await this.userService.toggleUserActive(body.userId, body.isActive);
+ // 移除密码字段,保证安全
+ const { password, ...safe } = data as any;
+ return successResponse(safe);
+ } catch (error) {
+ return errorResponse(error?.message || '操作失败');
+ }
+ }
+
+ // 更新用户(支持用户名/密码/权限/角色更新)
+ @Post('/update/:id')
+ async updateUser(
+ @Body() body: { username?: string; password?: string; email?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
+ @Query('id') id?: number
+ ) {
+ try {
+ // 条件判断:优先从路径参数获取 ID(兼容生成的 API 文件为 POST /user/update/:id)
+ const userId = Number((this.ctx?.params?.id ?? id));
+ if (!userId) throw new Error('缺少用户ID');
+ const data = await this.userService.updateUser(userId, body);
+ // 移除密码字段,保证安全
+ const { password, ...safe } = data as any;
+ return successResponse(safe);
+ } catch (error) {
+ return errorResponse(error?.message || '更新失败');
+ }
}
@ApiOkResponse()
@Get()
async getUser(@User() user) {
try {
- return successResponse(await this.userService.getUser(user.id));
+ // 详情移除密码字段
+ const data = await this.userService.getUser(user.id);
+ const { password, ...safe } = (data as any) || {};
+ return successResponse(safe);
} catch (error) {
return errorResponse('获取失败');
}
diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts
index d15da14..c4398b0 100644
--- a/src/controller/webhook.controller.ts
+++ b/src/controller/webhook.controller.ts
@@ -9,8 +9,7 @@ import {
} from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import * as crypto from 'crypto';
-import { WpProductService } from '../service/wp_product.service';
-import { WPService } from '../service/wp.service';
+
import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service';
@@ -18,11 +17,7 @@ import { OrderService } from '../service/order.service';
export class WebhookController {
private secret = 'YOONE24kd$kjcdjflddd';
- @Inject()
- private readonly wpProductService: WpProductService;
-
- @Inject()
- private readonly wpApiService: WPService;
+ // 平台服务保留按需注入
@Inject()
private readonly orderService: OrderService;
@@ -33,7 +28,7 @@ export class WebhookController {
@Inject()
private readonly siteService: SiteService;
- // 移除配置中的站点数组,来源统一改为数据库
+ // 移除配置中的站点数组,来源统一改为数据库
@Get('/')
async test() {
@@ -43,14 +38,15 @@ export class WebhookController {
@Post('/woocommerce')
async handleWooWebhook(
@Body() body: any,
- @Query('siteId') siteId: string,
+ @Query('siteId') siteIdStr: string,
@Headers() header: any
) {
const signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source'];
+ const siteId = Number(siteIdStr);
// 从数据库获取站点配置
- const site = await this.siteService.get(Number(siteId), true);
+ const site = await this.siteService.get(siteId, true);
if (!site || !source.includes(site.apiUrl)) {
console.log('domain not match');
@@ -78,32 +74,10 @@ export class WebhookController {
switch (topic) {
case 'product.created':
case 'product.updated':
- // 变体更新
- if (body.type === 'variation') {
- const variation = await this.wpApiService.getVariation(
- site,
- body.parent_id,
- body.id
- );
- this.wpProductService.syncVariation(
- siteId,
- body.parent_id,
- variation
- );
- break;
- }
- const variations =
- body.type === 'variable'
- ? await this.wpApiService.getVariations(site, body.id)
- : [];
- await this.wpProductService.syncProductAndVariations(
- String(site.id),
- body,
- variations
- );
+ // 不再写入本地,平台事件仅确认接收
break;
case 'product.deleted':
- await this.wpProductService.delWpProduct(String(site.id), body.id);
+ // 不再写入本地,平台事件仅确认接收
break;
case 'order.created':
case 'order.updated':
diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts
index 8623b1d..327c6a2 100644
--- a/src/controller/wp_product.controller.ts
+++ b/src/controller/wp_product.controller.ts
@@ -7,6 +7,8 @@ import {
Query,
Put,
Body,
+ Files,
+ Del,
} from '@midwayjs/core';
import { WpProductService } from '../service/wp_product.service';
import { errorResponse, successResponse } from '../utils/response.util';
@@ -14,71 +16,125 @@ import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes, WpProductListRes } from '../dto/reponse.dto';
import {
QueryWpProductDTO,
- SetConstitutionDTO,
UpdateVariationDTO,
UpdateWpProductDTO,
+ BatchSyncProductsDTO,
+ BatchUpdateTagsDTO,
+ BatchUpdateProductsDTO,
} from '../dto/wp_product.dto';
-import { WPService } from '../service/wp.service';
-import { SiteService } from '../service/site.service';
+
import {
ProductsRes,
} from '../dto/reponse.dto';
@Controller('/wp_product')
export class WpProductController {
- // 移除控制器内的配置站点引用,统一由服务层处理站点数据
+ // 移除控制器内的配置站点引用,统一由服务层处理站点数据
@Inject()
private readonly wpProductService: WpProductService;
- @Inject()
- private readonly wpApiService: WPService;
+ // 平台服务保留按需注入
- @Inject()
- private readonly siteService: SiteService;
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Del('/:id')
+ async delete(@Param('id') id: number) {
+ return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 删除');
+ }
+
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/import/:siteId')
+ async importProducts(@Param('siteId') siteId: number, @Files() files) {
+ try {
+ if (!files || files.length === 0) {
+ throw new Error('请上传文件');
+ }
+ await this.wpProductService.importProducts(siteId, files[0]);
+ return successResponse(true);
+ } catch (error) {
+ console.error('导入失败:', error);
+ return errorResponse(error.message || '导入失败');
+ }
+ }
+
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/setconstitution')
+ async setConstitution(@Body() body: any) {
+ try {
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error.message || '设置失败');
+ }
+ }
+
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/batch-update')
+ async batchUpdateProducts(@Body() body: BatchUpdateProductsDTO) {
+ try {
+ await this.wpProductService.batchUpdateProducts(body);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error.message || '批量更新失败');
+ }
+ }
+
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/batch-update-tags')
+ async batchUpdateTags(@Body() body: BatchUpdateTagsDTO) {
+ try {
+ await this.wpProductService.batchUpdateTags(body.ids, body.tags);
+ return successResponse(true);
+ } catch (error) {
+ return errorResponse(error.message || '批量更新标签失败');
+ }
+ }
@ApiOkResponse({
type: BooleanRes,
})
@Post('/sync/:siteId')
- async syncProducts(@Param('siteId') siteId: string) {
+ async syncProducts(@Param('siteId') siteId: number) {
try {
- await this.wpProductService.syncSite(siteId);
- return successResponse(true);
+ const result = await this.wpProductService.syncSite(siteId);
+ return successResponse(result);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
}
}
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/batch-sync-to-site/:siteId')
+ async batchSyncToSite(
+ @Param('siteId') siteId: number,
+ @Body() body: BatchSyncProductsDTO
+ ) {
+ try {
+ await this.wpProductService.batchSyncToSite(siteId, body.productIds);
+ return successResponse(true, '批量同步成功');
+ } catch (error) {
+ console.error('批量同步失败:', error);
+ return errorResponse(error.message || '批量同步失败');
+ }
+ }
+
@ApiOkResponse({
type: WpProductListRes,
})
@Get('/list')
async getWpProducts(@Query() query: QueryWpProductDTO) {
- try {
- const data = await this.wpProductService.getProductList(query);
- return successResponse(data);
- } catch (error) {
- return errorResponse(error.message);
- }
- }
-
- @ApiOkResponse({
- type: BooleanRes,
- })
- @Put('/:id/constitution')
- async setConstitution(
- @Param('id') id: number,
- @Body()
- body: SetConstitutionDTO
- ) {
- const { isProduct, constitution } = body;
- try {
- await this.wpProductService.setConstitution(id, isProduct, constitution);
- return successResponse(true);
- } catch (error) {
- return errorResponse(error.message);
- }
+ return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 列表');
}
@ApiOkResponse({
@@ -97,6 +153,22 @@ export class WpProductController {
}
}
+ /**
+ * 创建产品接口
+ * @param siteId 站点 ID
+ * @param body 创建数据
+ */
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/siteId/:siteId/products')
+ async createProduct(
+ @Param('siteId') siteId: number,
+ @Body() body: any
+ ) {
+ return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 创建');
+ }
+
/**
* 更新产品接口
* @param productId 产品 ID
@@ -107,33 +179,23 @@ export class WpProductController {
})
@Put('/siteId/:siteId/products/:productId')
async updateProduct(
- @Param('siteId') siteId: string,
+ @Param('siteId') siteId: number,
@Param('productId') productId: string,
@Body() body: UpdateWpProductDTO
) {
+ return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:id 更新');
+ }
+
+ @ApiOkResponse({
+ type: BooleanRes,
+ })
+ @Post('/sync-to-product/:id')
+ async syncToProduct(@Param('id') id: number) {
try {
- const isDuplicate = await this.wpProductService.isSkuDuplicate(
- body.sku,
- siteId,
- productId
- );
- if (isDuplicate) {
- return errorResponse('SKU已存在');
- }
- const site = await this.siteService.get(Number(siteId), true);
- const result = await this.wpApiService.updateProduct(
- site,
- productId,
- body
- );
- if (result) {
- this.wpProductService.updateWpProduct(siteId, productId, body);
- return successResponse(result, '产品更新成功');
- }
- return errorResponse('产品更新失败');
+ await this.wpProductService.syncToProduct(id);
+ return successResponse(true);
} catch (error) {
- console.error('更新产品失败:', error);
- return errorResponse(error.message || '产品更新失败');
+ return errorResponse(error.message);
}
}
@@ -145,42 +207,12 @@ export class WpProductController {
*/
@Put('/siteId/:siteId/products/:productId/variations/:variationId')
async updateVariation(
- @Param('siteId') siteId: string,
+ @Param('siteId') siteId: number,
@Param('productId') productId: string,
@Param('variationId') variationId: string,
@Body() body: UpdateVariationDTO
) {
- try {
- const isDuplicate = await this.wpProductService.isSkuDuplicate(
- body.sku,
- siteId,
- productId,
- variationId
- );
- if (isDuplicate) {
- return errorResponse('SKU已存在');
- }
- const site = await this.siteService.get(Number(siteId), true);
- const result = await this.wpApiService.updateVariation(
- site,
- productId,
- variationId,
- body
- );
- if (result) {
- this.wpProductService.updateWpProductVaritation(
- siteId,
- productId,
- variationId,
- body
- );
- return successResponse(result, '产品变体更新成功');
- }
- return errorResponse('变体更新失败');
- } catch (error) {
- console.error('更新变体失败:', error);
- return errorResponse(error.message || '产品变体更新失败');
- }
+ return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:productId/variations/:variationId 更新');
}
@ApiOkResponse({
diff --git a/src/db/datasource.ts b/src/db/datasource.ts
new file mode 100644
index 0000000..8dcfd63
--- /dev/null
+++ b/src/db/datasource.ts
@@ -0,0 +1,20 @@
+import { DataSource, DataSourceOptions } from 'typeorm';
+import { SeederOptions } from 'typeorm-extension';
+
+
+const options: DataSourceOptions & SeederOptions = {
+ type: 'mysql',
+ host: '127.0.0.1',
+ port: 23306,
+ username: 'root',
+ password: '12345678',
+ database: 'inventory',
+ synchronize: true,
+ logging: true,
+ entities: [__dirname + '/../entity/*.ts'],
+ migrations: ['src/db/migrations/**/*.ts'],
+ seeds: ['src/db/seeds/**/*.ts'],
+};
+
+export const AppDataSource = new DataSource(options);
+
diff --git a/src/db/migrations/1764238434984-product-dict-item-many-to-many.ts b/src/db/migrations/1764238434984-product-dict-item-many-to-many.ts
new file mode 100644
index 0000000..1011b18
--- /dev/null
+++ b/src/db/migrations/1764238434984-product-dict-item-many-to-many.ts
@@ -0,0 +1,32 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ProductDictItemManyToMany1764238434984 implements MigrationInterface {
+ name = 'ProductDictItemManyToMany1764238434984'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE \`product_attributes_dict_item\` (\`productId\` int NOT NULL, \`dictItemId\` int NOT NULL, INDEX \`IDX_592cdbdaebfec346c202ffb82c\` (\`productId\`), INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` (\`dictItemId\`), PRIMARY KEY (\`productId\`, \`dictItemId\`)) ENGINE=InnoDB`);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`brandId\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`flavorsId\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`strengthId\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`humidity\``);
+ await queryRunner.query(`ALTER TABLE \`product\` ADD \`sku\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`product\` ADD UNIQUE INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\` (\`sku\`)`);
+ await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_592cdbdaebfec346c202ffb82ca\` FOREIGN KEY (\`productId\`) REFERENCES \`product\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_406c1da5b6de45fecb7967c3ec0\` FOREIGN KEY (\`dictItemId\`) REFERENCES \`dict_item\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_406c1da5b6de45fecb7967c3ec0\``);
+ await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_592cdbdaebfec346c202ffb82ca\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`sku\``);
+ await queryRunner.query(`ALTER TABLE \`product\` ADD \`humidity\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`product\` ADD \`strengthId\` int NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`product\` ADD \`flavorsId\` int NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`product\` ADD \`brandId\` int NOT NULL`);
+ await queryRunner.query(`DROP INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` ON \`product_attributes_dict_item\``);
+ await queryRunner.query(`DROP INDEX \`IDX_592cdbdaebfec346c202ffb82c\` ON \`product_attributes_dict_item\``);
+ await queryRunner.query(`DROP TABLE \`product_attributes_dict_item\``);
+ }
+
+}
diff --git a/src/db/migrations/1764294088896-Area.ts b/src/db/migrations/1764294088896-Area.ts
new file mode 100644
index 0000000..e2948bb
--- /dev/null
+++ b/src/db/migrations/1764294088896-Area.ts
@@ -0,0 +1,45 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Area1764294088896 implements MigrationInterface {
+ name = 'Area1764294088896'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ // await queryRunner.query(`DROP INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\``);
+ // await queryRunner.query(`CREATE TABLE \`area\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_644ffaf8fbde4db798cb47712f\` (\`name\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
+ // await queryRunner.query(`CREATE TABLE \`stock_point_areas_area\` (\`stockPointId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_07d2db2150151e2ef341d2f1de\` (\`stockPointId\`), INDEX \`IDX_92707ea81fc19dc707dba24819\` (\`areaId\`), PRIMARY KEY (\`stockPointId\`, \`areaId\`)) ENGINE=InnoDB`);
+ // await queryRunner.query(`CREATE TABLE \`site_areas_area\` (\`siteId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_926a14ac4c91f38792831acd2a\` (\`siteId\`), INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` (\`areaId\`), PRIMARY KEY (\`siteId\`, \`areaId\`)) ENGINE=InnoDB`);
+ // await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`name\``);
+ // await queryRunner.query(`ALTER TABLE `product` ADD `promotionPrice` decimal(10,2) NOT NULL DEFAULT '0.00'`);
+ // await queryRunner.query(`ALTER TABLE `product` ADD `source` int NOT NULL DEFAULT '0'`);
+ // await queryRunner.query(`ALTER TABLE \`site\` ADD \`token\` varchar(255) NULL`);
+ // await queryRunner.query(`ALTER TABLE `site` ADD `name` varchar(255) NOT NULL`);
+ // await queryRunner.query(`ALTER TABLE \`site\` ADD UNIQUE INDEX \`IDX_9669a09fcc0eb6d2794a658f64\` (\`name\`)`);
+ await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` ADD CONSTRAINT \`FK_07d2db2150151e2ef341d2f1de1\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` ADD CONSTRAINT \`FK_92707ea81fc19dc707dba24819c\` FOREIGN KEY (\`areaId\`) REFERENCES \`area\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ await queryRunner.query(`ALTER TABLE \`site_areas_area\` ADD CONSTRAINT \`FK_926a14ac4c91f38792831acd2a6\` FOREIGN KEY (\`siteId\`) REFERENCES \`site\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ await queryRunner.query(`ALTER TABLE \`site_areas_area\` ADD CONSTRAINT \`FK_7c26c582048e3ecd3cd5938cb9f\` FOREIGN KEY (\`areaId\`) REFERENCES \`area\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`site_areas_area\` DROP FOREIGN KEY \`FK_7c26c582048e3ecd3cd5938cb9f\``);
+ await queryRunner.query(`ALTER TABLE \`site_areas_area\` DROP FOREIGN KEY \`FK_926a14ac4c91f38792831acd2a6\``);
+ await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` DROP FOREIGN KEY \`FK_92707ea81fc19dc707dba24819c\``);
+ await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` DROP FOREIGN KEY \`FK_07d2db2150151e2ef341d2f1de1\``);
+ await queryRunner.query(`ALTER TABLE \`site\` DROP INDEX \`IDX_9669a09fcc0eb6d2794a658f64\``);
+ await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`name\``);
+ await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`token\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`source\``);
+ await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`promotionPrice\``);
+ await queryRunner.query(`ALTER TABLE \`site\` ADD \`name\` varchar(255) NOT NULL`);
+ await queryRunner.query(`DROP INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` ON \`site_areas_area\``);
+ await queryRunner.query(`DROP INDEX \`IDX_926a14ac4c91f38792831acd2a\` ON \`site_areas_area\``);
+ await queryRunner.query(`DROP TABLE \`site_areas_area\``);
+ await queryRunner.query(`DROP INDEX \`IDX_92707ea81fc19dc707dba24819\` ON \`stock_point_areas_area\``);
+ await queryRunner.query(`DROP INDEX \`IDX_07d2db2150151e2ef341d2f1de\` ON \`stock_point_areas_area\``);
+ await queryRunner.query(`DROP TABLE \`stock_point_areas_area\``);
+ await queryRunner.query(`DROP INDEX \`IDX_644ffaf8fbde4db798cb47712f\` ON \`area\``);
+ await queryRunner.query(`DROP TABLE \`area\``);
+ await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\` (\`name\`)`);
+ }
+
+}
diff --git a/src/db/migrations/1764299629279-ProductStock.ts b/src/db/migrations/1764299629279-ProductStock.ts
new file mode 100644
index 0000000..9767e45
--- /dev/null
+++ b/src/db/migrations/1764299629279-ProductStock.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ProductStock1764299629279 implements MigrationInterface {
+ name = 'ProductStock1764299629279'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE \`order_item_original\` (\`id\` int NOT NULL AUTO_INCREMENT, \`order_id\` int NOT NULL, \`name\` varchar(255) NOT NULL, \`siteId\` varchar(255) NOT NULL, \`externalOrderId\` varchar(255) NOT NULL, \`externalOrderItemId\` varchar(255) NULL, \`externalProductId\` varchar(255) NOT NULL, \`externalVariationId\` varchar(255) NOT NULL, \`quantity\` int NOT NULL, \`subtotal\` decimal(10,2) NULL, \`subtotal_tax\` decimal(10,2) NULL, \`total\` decimal(10,2) NULL, \`total_tax\` decimal(10,2) NULL, \`sku\` varchar(255) NULL, \`price\` decimal(10,2) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD CONSTRAINT \`FK_ca48e4bce0bb8cecd24cc8081e5\` FOREIGN KEY (\`order_id\`) REFERENCES \`order\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP FOREIGN KEY \`FK_ca48e4bce0bb8cecd24cc8081e5\``);
+ await queryRunner.query(`DROP TABLE \`order_item_original\``);
+ }
+
+}
diff --git a/src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts b/src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts
new file mode 100644
index 0000000..e71f676
--- /dev/null
+++ b/src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts
@@ -0,0 +1,46 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class UpdateDictItemUniqueConstraint1764569947170 implements MigrationInterface {
+ name = 'UpdateDictItemUniqueConstraint1764569947170'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
+ }
+
+}
diff --git a/src/db/migrations/1765275715762-add_test_data_to_template.ts b/src/db/migrations/1765275715762-add_test_data_to_template.ts
new file mode 100644
index 0000000..24956b6
--- /dev/null
+++ b/src/db/migrations/1765275715762-add_test_data_to_template.ts
@@ -0,0 +1,68 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddTestDataToTemplate1765275715762 implements MigrationInterface {
+ name = 'AddTestDataToTemplate1765275715762'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` DROP FOREIGN KEY \`FK_e93d8c42c9baf5a0dade42c59ae\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
+ await queryRunner.query(`ALTER TABLE \`template\` ADD \`testData\` text NULL COMMENT '测试数据JSON'`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` ADD CONSTRAINT \`FK_e93d8c42c9baf5a0dade42c59ae\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` DROP FOREIGN KEY \`FK_e93d8c42c9baf5a0dade42c59ae\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
+ await queryRunner.query(`ALTER TABLE \`template\` DROP COLUMN \`testData\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
+ await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` ADD CONSTRAINT \`FK_e93d8c42c9baf5a0dade42c59ae\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
+ }
+
+}
diff --git a/src/db/migrations/1765330208213-add-site-description.ts b/src/db/migrations/1765330208213-add-site-description.ts
new file mode 100644
index 0000000..148ce00
--- /dev/null
+++ b/src/db/migrations/1765330208213-add-site-description.ts
@@ -0,0 +1,46 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddSiteDescription1765330208213 implements MigrationInterface {
+ name = 'AddSiteDescription1765330208213'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
+ }
+
+}
diff --git a/src/db/seeds/area.seeder.ts b/src/db/seeds/area.seeder.ts
new file mode 100644
index 0000000..6adb8ec
--- /dev/null
+++ b/src/db/seeds/area.seeder.ts
@@ -0,0 +1,34 @@
+
+import { Seeder, SeederFactoryManager } from 'typeorm-extension';
+import { DataSource } from 'typeorm';
+import { Area } from '../../entity/area.entity';
+
+export default class AreaSeeder implements Seeder {
+ public async run(
+ dataSource: DataSource,
+ factoryManager: SeederFactoryManager
+ ): Promise {
+ const areaRepository = dataSource.getRepository(Area);
+
+ const areas = [
+ { name: 'Australia', code: 'AU' },
+ { name: 'Canada', code: 'CA' },
+ { name: 'United States', code: 'US' },
+ { name: 'Germany', code: 'DE' },
+ { name: 'Poland', code: 'PL' },
+ ];
+
+ for (const areaData of areas) {
+ const existingArea = await areaRepository.findOne({
+ where: [
+ { name: areaData.name },
+ { code: areaData.code }
+ ]
+ });
+ if (!existingArea) {
+ const newArea = areaRepository.create(areaData);
+ await areaRepository.save(newArea);
+ }
+ }
+ }
+}
diff --git a/src/db/seeds/category.seeder.ts b/src/db/seeds/category.seeder.ts
new file mode 100644
index 0000000..4763d60
--- /dev/null
+++ b/src/db/seeds/category.seeder.ts
@@ -0,0 +1,39 @@
+import { Seeder } from 'typeorm-extension';
+import { DataSource } from 'typeorm';
+import { Category } from '../../entity/category.entity';
+
+export default class CategorySeeder implements Seeder {
+ public async run(
+ dataSource: DataSource,
+ ): Promise {
+ const repository = dataSource.getRepository(Category);
+
+ const categories = [
+ {
+ name: 'nicotine-pouches',
+ title: 'Nicotine Pouches',
+ titleCN: '尼古丁袋',
+ sort: 1
+ },
+ {
+ name: 'vape',
+ title: 'vape',
+ titleCN: '电子烟',
+ sort: 2
+ },
+ {
+ name: 'pouches-can',
+ title: 'Pouches Can',
+ titleCN: '口含烟盒',
+ sort: 3
+ },
+ ];
+
+ for (const cat of categories) {
+ const existing = await repository.findOne({ where: { name: cat.name } });
+ if (!existing) {
+ await repository.save(cat);
+ }
+ }
+ }
+}
diff --git a/src/db/seeds/category_attribute.seeder.ts b/src/db/seeds/category_attribute.seeder.ts
new file mode 100644
index 0000000..6f11ea5
--- /dev/null
+++ b/src/db/seeds/category_attribute.seeder.ts
@@ -0,0 +1,62 @@
+import { Seeder } from 'typeorm-extension';
+import { DataSource } from 'typeorm';
+import { Dict } from '../../entity/dict.entity';
+import { Category } from '../../entity/category.entity';
+import { CategoryAttribute } from '../../entity/category_attribute.entity';
+
+export default class CategoryAttributeSeeder implements Seeder {
+ public async run(
+ dataSource: DataSource,
+ ): Promise {
+ const dictRepository = dataSource.getRepository(Dict);
+ const categoryRepository = dataSource.getRepository(Category);
+ const categoryAttributeRepository = dataSource.getRepository(CategoryAttribute);
+
+ // 1. 确保属性字典存在
+ const attributeNames = ['brand', 'strength', 'flavor', 'size', 'humidity'];
+ const attributeDicts: Dict[] = [];
+
+ for (const name of attributeNames) {
+ let dict = await dictRepository.findOne({ where: { name } });
+ if (!dict) {
+ dict = new Dict();
+ dict.name = name;
+ dict.title = name.charAt(0).toUpperCase() + name.slice(1);
+ dict.deletable = false;
+ dict = await dictRepository.save(dict);
+ console.log(`Created Dict: ${name}`);
+ }
+ attributeDicts.push(dict);
+ }
+
+ // 2. 获取 'nicotine-pouches' 分类 (由 CategorySeeder 创建)
+ const nicotinePouchesCategory = await categoryRepository.findOne({
+ where: {
+ name: 'nicotine-pouches'
+ }
+ });
+
+ if (!nicotinePouchesCategory) {
+ console.warn('Category "nicotine-pouches" not found. Skipping attribute linking. Please ensure CategorySeeder runs first.');
+ return;
+ }
+
+ // 3. 绑定属性到 'nicotine-pouches' 分类
+ for (const attrDict of attributeDicts) {
+ const existing = await categoryAttributeRepository.findOne({
+ where: {
+ category: { id: nicotinePouchesCategory.id },
+ attributeDict: { id: attrDict.id }
+ }
+ });
+
+ if (!existing) {
+ const link = new CategoryAttribute();
+ link.category = nicotinePouchesCategory;
+ link.attributeDict = attrDict;
+ await categoryAttributeRepository.save(link);
+ console.log(`Linked ${attrDict.name} to ${nicotinePouchesCategory.name}`);
+ }
+ }
+ }
+}
diff --git a/src/db/seeds/dict.seeder.ts b/src/db/seeds/dict.seeder.ts
new file mode 100644
index 0000000..0201bd7
--- /dev/null
+++ b/src/db/seeds/dict.seeder.ts
@@ -0,0 +1,175 @@
+import { Seeder } from 'typeorm-extension';
+import { DataSource } from 'typeorm';
+import { Dict } from '../../entity/dict.entity';
+import { DictItem } from '../../entity/dict_item.entity';
+
+export default class DictSeeder implements Seeder {
+ /**
+ * 格式化名称为 kebab-case
+ * @param name 需要格式化的名称
+ * @returns 格式化后的名称
+ */
+ private formatName(name: string): string {
+ // return String(name).replace(/[\_\s.]+/g, '-').toLowerCase();
+ // 只替换空格和下划线
+ return String(name).replace(/[\_\s]+/g, '-').toLowerCase();
+ }
+
+ public async run(
+ dataSource: DataSource,
+ ): Promise {
+ const dictRepository = dataSource.getRepository(Dict);
+ const dictItemRepository = dataSource.getRepository(DictItem);
+
+ const flavorsData = [
+ { name: 'bellini', title: 'Bellini', titleCn: '贝利尼', shortName: 'BL' },
+ { name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷', shortName: 'MP' },
+ { name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓', shortName: 'BB' },
+ { name: 'citrus', title: 'Citrus', titleCn: '柑橘', shortName: 'CT' },
+ { name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷', shortName: 'WG' },
+ { name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷', shortName: 'CM' },
+ { name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃', shortName: 'JP' },
+ { name: 'orange', title: 'ORANGE', titleCn: '橙子', shortName: 'OR' },
+ { name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷', shortName: 'PP' },
+ { name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷', shortName: 'SM' },
+ { name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓', shortName: 'SB' },
+ { name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜', shortName: 'WM' },
+ { name: 'coffee', title: 'COFFEE', titleCn: '咖啡', shortName: 'CF' },
+ { name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水', shortName: 'LN' },
+ { name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷', shortName: 'AM' },
+ { name: 'peach', title: 'PEACH', titleCn: '桃子', shortName: 'PC' },
+ { name: 'mango', title: 'Mango', titleCn: '芒果', shortName: 'MG' },
+ { name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷', shortName: 'IWG' },
+ { name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水', shortName: 'PLN' },
+ { name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃', shortName: 'BC' },
+ { name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷', shortName: 'FM' },
+ { name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝', shortName: 'SBL' },
+ { name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果', shortName: 'PF' },
+ { name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰', shortName: 'BI' },
+ { name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖', shortName: 'BG' },
+ { name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰', shortName: 'MI' },
+ { name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰', shortName: 'GI' },
+ { name: 'apple', title: 'apple', titleCn: '苹果', shortName: 'AP' },
+ { name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
+ { name: 'cherry', title: 'cherry', titleCn: '樱桃', shortName: 'CH' },
+ { name: 'lemon', title: 'lemon', titleCn: '柠檬', shortName: 'LM' },
+ { name: 'razz', title: 'razz', titleCn: '覆盆子', shortName: 'RZ' },
+ { name: 'pineapple', title: 'pineapple', titleCn: '菠萝', shortName: 'PA' },
+ { name: 'berry', title: 'berry', titleCn: '浆果', shortName: 'BR' },
+ { name: 'fruit', title: 'fruit', titleCn: '水果', shortName: 'FR' },
+ { name: 'mint', title: 'mint', titleCn: '薄荷', shortName: 'MT' },
+ { name: 'menthol', title: 'menthol', titleCn: '薄荷醇', shortName: 'MH' },
+ ];
+
+ const brandsData = [
+ { name: 'yoone', title: 'Yoone', titleCn: '', shortName: 'YN' },
+ { name: 'white-fox', title: 'White Fox', titleCn: '', shortName: 'WF' },
+ { name: 'zyn', title: 'ZYN', titleCn: '', shortName: 'ZN' },
+ { name: 'zonnic', title: 'Zonnic', titleCn: '', shortName: 'ZC' },
+ { name: 'zolt', title: 'Zolt', titleCn: '', shortName: 'ZT' },
+ { name: 'velo', title: 'Velo', titleCn: '', shortName: 'VL' },
+ { name: 'lucy', title: 'Lucy', titleCn: '', shortName: 'LC' },
+ { name: 'egp', title: 'EGP', titleCn: '', shortName: 'EP' },
+ { name: 'bridge', title: 'Bridge', titleCn: '', shortName: 'BR' },
+ { name: 'zex', title: 'ZEX', titleCn: '', shortName: 'ZX' },
+ { name: 'sesh', title: 'Sesh', titleCn: '', shortName: 'SH' },
+ { name: 'pablo', title: 'Pablo', titleCn: '', shortName: 'PB' },
+ ];
+
+ const strengthsData = [
+ { name: '2mg', title: '2MG', titleCn: '2毫克', shortName: '2M' },
+ { name: '3mg', title: '3MG', titleCn: '3毫克', shortName: '3M' },
+ { name: '4mg', title: '4MG', titleCn: '4毫克', shortName: '4M' },
+ { name: '6mg', title: '6MG', titleCn: '6毫克', shortName: '6M' },
+ { name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克', shortName: '6.5M' },
+ { name: '9mg', title: '9MG', titleCn: '9毫克', shortName: '9M' },
+ { name: '12mg', title: '12MG', titleCn: '12毫克', shortName: '12M' },
+ { name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克', shortName: '16.5M' },
+ { name: '18mg', title: '18MG', titleCn: '18毫克', shortName: '18M' },
+ { name: '30mg', title: '30MG', titleCn: '30毫克', shortName: '30M' },
+ ];
+
+ // 初始化语言字典
+ const locales = [
+ { name: 'zh-cn', title: '简体中文', titleCn: '简体中文', shortName: 'CN' },
+ { name: 'en-us', title: 'English', titleCn: '英文', shortName: 'EN' },
+ ];
+
+ for (const locale of locales) {
+ await this.createOrFindDict(dictRepository, locale);
+ }
+
+ // 添加示例翻译条目
+ const zhDict = await dictRepository.findOne({ where: { name: 'zh-cn' } });
+ const enDict = await dictRepository.findOne({ where: { name: 'en-us' } });
+
+ const translations = [
+ { name: 'common-save', zh: '保存', en: 'Save' },
+ { name: 'common-cancel', zh: '取消', en: 'Cancel' },
+ { name: 'common-success', zh: '操作成功', en: 'Success' },
+ { name: 'common-failure', zh: '操作失败', en: 'Failure' },
+ ];
+
+ for (const t of translations) {
+ // 添加中文翻译
+ let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
+ if (!item) {
+ await dictItemRepository.save({ name: t.name, title: t.zh, titleCn: t.zh, shortName: t.zh.substring(0, 2).toUpperCase(), dict: zhDict });
+ }
+
+ // 添加英文翻译
+ item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
+ if (!item) {
+ await dictItemRepository.save({ name: t.name, title: t.en, titleCn: t.en, shortName: t.en.substring(0, 2).toUpperCase(), dict: enDict });
+ }
+ }
+
+ const brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌', shortName: 'BR' });
+ const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味', shortName: 'FL' });
+ const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度', shortName: 'ST' });
+
+ // 遍历品牌数据
+ await this.seedDictItems(dictItemRepository, brandDict, brandsData);
+
+ // 遍历口味数据
+ await this.seedDictItems(dictItemRepository, flavorDict, flavorsData);
+
+ // 遍历强度数据
+ await this.seedDictItems(dictItemRepository, strengthDict, strengthsData);
+ }
+
+ /**
+ * 创建或查找字典
+ * @param repo DictRepository
+ * @param dictInfo 字典信息
+ * @returns Dict 实例
+ */
+ private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string; shortName: string }): Promise {
+ // 格式化 name
+ const formattedName = this.formatName(dictInfo.name);
+ let dict = await repo.findOne({ where: { name: formattedName } });
+ if (!dict) {
+ // 如果字典不存在,则使用格式化后的 name 创建新字典
+ dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn, shortName: dictInfo.shortName });
+ }
+ return dict;
+ }
+
+ /**
+ * 填充字典项
+ * @param repo DictItemRepository
+ * @param dict 字典实例
+ * @param items 字典项数组
+ */
+ private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string; shortName: string }[]): Promise {
+ for (const item of items) {
+ // 格式化 name
+ const formattedName = this.formatName(item.name);
+ const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
+ if (!existingItem) {
+ // 如果字典项不存在,则使用格式化后的 name 创建新字典项
+ await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, shortName: item.shortName, dict });
+ }
+ }
+ }
+}
diff --git a/src/db/seeds/template.seeder.ts b/src/db/seeds/template.seeder.ts
new file mode 100644
index 0000000..be51218
--- /dev/null
+++ b/src/db/seeds/template.seeder.ts
@@ -0,0 +1,72 @@
+import { Seeder, SeederFactoryManager } from 'typeorm-extension';
+import { DataSource } from 'typeorm';
+import { Template } from '../../entity/template.entity';
+
+/**
+ * @class TemplateSeeder
+ * @description 模板数据填充器,用于在数据库初始化时插入默认的模板数据.
+ */
+export default class TemplateSeeder implements Seeder {
+ /**
+ * @method run
+ * @description 执行数据填充操作.如果模板不存在,则创建它;如果存在,则更新它.
+ * @param {DataSource} dataSource - 数据源实例,用于获取 repository.
+ * @param {SeederFactoryManager} factoryManager - Seeder 工厂管理器.
+ */
+ public async run(
+ dataSource: DataSource,
+ factoryManager: SeederFactoryManager
+ ): Promise {
+ // 获取 Template 实体的 repository
+ const templateRepository = dataSource.getRepository(Template);
+
+ const templates = [
+ {
+ name: 'product.sku',
+ value: '<%= it.brand %>-<%=it.category%>-<%= it.flavor %>-<%= it.strength %>-<%= it.humidity %>',
+ description: '产品SKU模板',
+ testData: JSON.stringify({
+ brand: 'Brand',
+ category: 'Category',
+ flavor: 'Flavor',
+ strength: '10mg',
+ humidity: 'Dry',
+ }),
+ },
+ {
+ name: 'product.title',
+ value: '<%= it.brand %> <%= it.flavor %> <%= it.strength %> <%= it.humidity %>',
+ description: '产品标题模板',
+ testData: JSON.stringify({
+ brand: 'Brand',
+ flavor: 'Flavor',
+ strength: '10mg',
+ humidity: 'Dry',
+ }),
+ },
+ ];
+
+ for (const t of templates) {
+ // 检查模板是否已存在
+ const existingTemplate = await templateRepository.findOne({
+ where: { name: t.name },
+ });
+
+ if (existingTemplate) {
+ // 如果存在,则更新
+ existingTemplate.value = t.value;
+ existingTemplate.description = t.description;
+ existingTemplate.testData = t.testData;
+ await templateRepository.save(existingTemplate);
+ } else {
+ // 如果不存在,则创建并保存
+ const template = new Template();
+ template.name = t.name;
+ template.value = t.value;
+ template.description = t.description;
+ template.testData = t.testData;
+ await templateRepository.save(template);
+ }
+ }
+ }
+}
diff --git a/src/dto/area.dto.ts b/src/dto/area.dto.ts
new file mode 100644
index 0000000..2523286
--- /dev/null
+++ b/src/dto/area.dto.ts
@@ -0,0 +1,29 @@
+
+import { ApiProperty } from '@midwayjs/swagger';
+import { Rule, RuleType } from '@midwayjs/validate';
+
+export class CreateAreaDTO {
+ @ApiProperty({ description: '编码' })
+ @Rule(RuleType.string().required())
+ code: string;
+}
+
+export class UpdateAreaDTO {
+ @ApiProperty({ description: '编码', required: false })
+ @Rule(RuleType.string())
+ code?: string;
+}
+
+export class QueryAreaDTO {
+ @ApiProperty({ description: '当前页', required: false, default: 1 })
+ @Rule(RuleType.number().integer().min(1).default(1))
+ currentPage?: number;
+
+ @ApiProperty({ description: '每页数量', required: false, default: 10 })
+ @Rule(RuleType.number().integer().min(1).default(10))
+ pageSize?: number;
+
+ @ApiProperty({ description: '关键词(名称或编码)', required: false })
+ @Rule(RuleType.string())
+ keyword?: string;
+}
diff --git a/src/dto/dict.dto.ts b/src/dto/dict.dto.ts
new file mode 100644
index 0000000..6fae6aa
--- /dev/null
+++ b/src/dto/dict.dto.ts
@@ -0,0 +1,62 @@
+import { Rule, RuleType } from '@midwayjs/validate';
+
+// 创建字典的数据传输对象
+export class CreateDictDTO {
+ @Rule(RuleType.string().required())
+ name: string; // 字典名称
+
+ @Rule(RuleType.string().required())
+ title: string; // 字典标题
+}
+
+// 更新字典的数据传输对象
+export class UpdateDictDTO {
+ @Rule(RuleType.string())
+ name?: string; // 字典名称 (可选)
+
+ @Rule(RuleType.string())
+ title?: string; // 字典标题 (可选)
+}
+
+// 创建字典项的数据传输对象
+export class CreateDictItemDTO {
+ @Rule(RuleType.string().required())
+ name: string; // 字典项名称
+
+ @Rule(RuleType.string().required())
+ title: string; // 字典项标题
+
+ @Rule(RuleType.string().allow('').allow(null))
+ titleCN?: string; // 字典项中文标题 (可选)
+
+ @Rule(RuleType.string().allow('').allow(null))
+ image?: string; // 图片 (可选)
+
+ @Rule(RuleType.string().allow('').allow(null))
+ shortName?: string; // 简称 (可选)
+
+ @Rule(RuleType.number().required())
+ dictId: number; // 所属字典的ID
+}
+
+// 更新字典项的数据传输对象
+export class UpdateDictItemDTO {
+ @Rule(RuleType.string())
+ name?: string; // 字典项名称 (可选)
+
+ @Rule(RuleType.string())
+ title?: string; // 字典项标题 (可选)
+
+ @Rule(RuleType.string().allow('').allow(null))
+ titleCN?: string; // 字典项中文标题 (可选)
+
+ @Rule(RuleType.string().allow(null))
+ value?: string; // 字典项值 (可选)
+
+ @Rule(RuleType.string().allow('').allow(null))
+ image?: string; // 图片 (可选)
+
+ @Rule(RuleType.string().allow('').allow(null))
+ shortName?: string; // 简称 (可选)
+
+}
diff --git a/src/dto/freightcom.dto.ts b/src/dto/freightcom.dto.ts
index 2bba475..32b998d 100644
--- a/src/dto/freightcom.dto.ts
+++ b/src/dto/freightcom.dto.ts
@@ -8,7 +8,7 @@ export type PackagingType =
// | PackagingCourierPak
// | PackagingEnvelope;
-// 定义包装类型的枚举,用于 API 文档描述
+// 定义包装类型的枚举,用于 API 文档描述
export enum PackagingTypeEnum {
Pallet = 'pallet',
Package = 'package',
diff --git a/src/dto/order.dto.ts b/src/dto/order.dto.ts
index 0f0b133..34e35db 100644
--- a/src/dto/order.dto.ts
+++ b/src/dto/order.dto.ts
@@ -61,8 +61,8 @@ export class QueryOrderDTO {
externalOrderId: string;
@ApiProperty()
- @Rule(RuleType.string())
- siteId: string;
+ @Rule(RuleType.number())
+ siteId: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
@@ -92,7 +92,7 @@ export class QueryOrderDTO {
@Rule(RuleType.string())
payment_method: string;
- @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
+ @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
@Rule(RuleType.bool().default(false))
isSubscriptionOnly?: boolean;
}
@@ -115,8 +115,8 @@ export class QueryOrderSalesDTO {
pageSize: number;
@ApiProperty()
- @Rule(RuleType.string())
- siteId: string;
+ @Rule(RuleType.number())
+ siteId: number;
@ApiProperty()
@Rule(RuleType.string())
@@ -156,8 +156,8 @@ export class QueryOrderItemDTO {
pageSize: number;
@ApiProperty()
- @Rule(RuleType.string().allow(''))
- siteId: string;
+ @Rule(RuleType.number().allow(''))
+ siteId: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts
index d952151..07a8df2 100644
--- a/src/dto/product.dto.ts
+++ b/src/dto/product.dto.ts
@@ -1,6 +1,31 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
+/**
+ * 属性输入DTO
+ */
+export class AttributeInputDTO {
+ @ApiProperty({ description: '属性字典标识', example: 'brand' })
+ @Rule(RuleType.string())
+ dictName?: string;
+
+ @ApiProperty({ description: '属性值', example: 'ZYN' })
+ @Rule(RuleType.string())
+ value?: string;
+
+ @ApiProperty({ description: '属性ID', example: 1 })
+ @Rule(RuleType.number())
+ id?: number;
+
+ @ApiProperty({ description: '属性名称', example: 'ZYN' })
+ @Rule(RuleType.string())
+ name?: string;
+
+ @ApiProperty({ description: '属性显示名称', example: 'ZYN' })
+ @Rule(RuleType.string())
+ title?: string;
+}
+
/**
* DTO 用于创建产品
*/
@@ -13,162 +38,248 @@ export class CreateProductDTO {
@Rule(RuleType.string().required().empty({ message: '产品名称不能为空' }))
name: string;
+ @ApiProperty({ description: '产品中文名称', required: false })
+ @Rule(RuleType.string().allow('').optional())
+ nameCn?: string;
+
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string())
description: string;
- @ApiProperty({ example: '1', description: '分类 ID' })
- @Rule(RuleType.number())
- categoryId: number;
+ @ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
+ @Rule(RuleType.string().optional())
+ shortDescription?: string;
- @ApiProperty()
- @Rule(RuleType.number())
- strengthId: number;
-
- @ApiProperty()
- @Rule(RuleType.number())
- flavorsId: number;
-
- @ApiProperty()
+ @ApiProperty({ description: '产品 SKU', required: false })
@Rule(RuleType.string())
- humidity: string;
+ sku?: string;
+
+ @ApiProperty({ description: '分类ID (DictItem ID)', required: false })
+ @Rule(RuleType.number())
+ categoryId?: number;
+
+ @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ siteSkus?: string[];
+
+ // 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
+ @ApiProperty({ description: '属性列表', type: 'array' })
+ @Rule(RuleType.array().required())
+ attributes: AttributeInputDTO[];
+
+ // 商品价格
+ @ApiProperty({ description: '价格', example: 99.99, required: false })
+ @Rule(RuleType.number())
+ price?: number;
+
+ // 促销价格
+ @ApiProperty({ description: '促销价格', example: 99.99, required: false })
+ @Rule(RuleType.number())
+ promotionPrice?: number;
+
+
+
+ // 商品类型(默认 single; bundle 需手动设置组成)
+ @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
+ @Rule(RuleType.string().valid('single', 'bundle').default('single'))
+ type?: string;
+
+ // 仅当 type 为 'bundle' 时,才需要提供 components
+ @ApiProperty({ description: '产品组成', type: 'array', required: false })
+ @Rule(
+ RuleType.array()
+ .items(
+ RuleType.object({
+ sku: RuleType.string().required(),
+ quantity: RuleType.number().required(),
+ })
+ )
+ .when('type', {
+ is: 'bundle',
+ then: RuleType.array().required(),
+ })
+ )
+ components?: { sku: string; quantity: number }[];
}
/**
* DTO 用于更新产品
*/
-export class UpdateProductDTO extends CreateProductDTO {
+export class UpdateProductDTO {
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
@Rule(RuleType.string())
- name: string;
+ name?: string;
+
+ @ApiProperty({ description: '产品中文名称', required: false })
+ @Rule(RuleType.string().allow('').optional())
+ nameCn?: string;
+
+ @ApiProperty({ example: '产品描述', description: '产品描述' })
+ @Rule(RuleType.string())
+ description?: string;
+
+ @ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
+ @Rule(RuleType.string().optional())
+ shortDescription?: string;
+
+ @ApiProperty({ description: '产品 SKU', required: false })
+ @Rule(RuleType.string())
+ sku?: string;
+
+ @ApiProperty({ description: '分类ID (DictItem ID)', required: false })
+ @Rule(RuleType.number())
+ categoryId?: number;
+
+ @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ siteSkus?: string[];
+
+ // 商品价格
+ @ApiProperty({ description: '价格', example: 99.99, required: false })
+ @Rule(RuleType.number())
+ price?: number;
+
+ // 促销价格
+ @ApiProperty({ description: '促销价格', example: 99.99, required: false })
+ @Rule(RuleType.number())
+ promotionPrice?: number;
+
+
+
+ // 属性更新(可选, 支持增量替换指定字典的属性项)
+ @ApiProperty({ description: '属性列表', type: 'array', required: false })
+ @Rule(RuleType.array())
+ attributes?: AttributeInputDTO[];
+
+ // 商品类型(single 或 bundle)
+ @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
+ @Rule(RuleType.string().valid('single', 'bundle'))
+ type?: string;
+
+ // 仅当 type 为 'bundle' 时,才需要提供 components
+ @ApiProperty({ description: '产品组成', type: 'array', required: false })
+ @Rule(
+ RuleType.array()
+ .items(
+ RuleType.object({
+ sku: RuleType.string().required(),
+ quantity: RuleType.number().required(),
+ })
+ )
+ .when('type', {
+ is: 'bundle',
+ then: RuleType.array().optional(),
+ })
+ )
+ components?: { sku: string; quantity: number }[];
+}
+
+
+/**
+ * DTO 用于批量更新产品属性
+ */
+export class BatchUpdateProductDTO {
+ @ApiProperty({ description: '产品ID列表', type: 'array', required: true })
+ @Rule(RuleType.array().items(RuleType.number()).required().min(1))
+ ids: number[];
+
+ @ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称', required: false })
+ @Rule(RuleType.string().optional())
+ name?: string;
+
+ @ApiProperty({ description: '产品中文名称', required: false })
+ @Rule(RuleType.string().allow('').optional())
+ nameCn?: string;
+
+ @ApiProperty({ example: '产品描述', description: '产品描述', required: false })
+ @Rule(RuleType.string().optional())
+ description?: string;
+
+ @ApiProperty({ example: '产品简短描述', description: '产品简短描述', required: false })
+ @Rule(RuleType.string().optional())
+ shortDescription?: string;
+
+ @ApiProperty({ description: '产品 SKU', required: false })
+ @Rule(RuleType.string().optional())
+ sku?: string;
+
+ @ApiProperty({ description: '分类ID (DictItem ID)', required: false })
+ @Rule(RuleType.number().optional())
+ categoryId?: number;
+
+ @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ siteSkus?: string[];
+
+ @ApiProperty({ description: '价格', example: 99.99, required: false })
+ @Rule(RuleType.number().optional())
+ price?: number;
+
+ @ApiProperty({ description: '促销价格', example: 99.99, required: false })
+ @Rule(RuleType.number().optional())
+ promotionPrice?: number;
+
+ @ApiProperty({ description: '属性列表', type: 'array', required: false })
+ @Rule(RuleType.array().optional())
+ attributes?: AttributeInputDTO[];
+
+ @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
+ @Rule(RuleType.string().valid('single', 'bundle').optional())
+ type?: string;
+}
+
+/**
+ * DTO 用于批量删除产品
+ */
+export class BatchDeleteProductDTO {
+ @ApiProperty({ description: '产品ID列表', type: 'array', required: true })
+ @Rule(RuleType.array().items(RuleType.number()).required().min(1))
+ ids: number[];
+}
+
+/**
+ * DTO 用于创建分类属性绑定
+ */
+export class CreateCategoryAttributeDTO {
+ @ApiProperty({ description: '分类字典项ID', example: 1 })
+ @Rule(RuleType.number().required())
+ categoryItemId: number;
+
+ @ApiProperty({ description: '属性字典ID列表', example: [2, 3] })
+ @Rule(RuleType.array().items(RuleType.number()).required())
+ attributeDictIds: number[];
}
/**
* DTO 用于分页查询产品
*/
export class QueryProductDTO {
- @ApiProperty({ example: '1', description: '页码' })
- @Rule(RuleType.number())
+ @ApiProperty({ description: '当前页', example: 1 })
+ @Rule(RuleType.number().default(1))
current: number;
- @ApiProperty({ example: '10', description: '每页大小' })
- @Rule(RuleType.number())
+ @ApiProperty({ description: '每页数量', example: 10 })
+ @Rule(RuleType.number().default(10))
pageSize: number;
- @ApiProperty({ example: 'ZYN', description: '关键字' })
+ @ApiProperty({ description: '搜索关键字', required: false })
@Rule(RuleType.string())
- name: string;
+ name?: string;
- @ApiProperty({ example: '1', description: '分类 ID' })
- @Rule(RuleType.string())
- categoryId: number;
-}
-
-/**
- * DTO 用于创建分类
- */
-export class CreateCategoryDTO {
- @ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
- @Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
- name: string;
-
- @Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
- unique_key: string;
-}
-
-/**
- * DTO 用于更新分类
- */
-export class UpdateCategoryDTO {
- @ApiProperty({ example: 'ZYN', description: '分类名称' })
- @Rule(RuleType.string())
- name: string;
-}
-
-/**
- * DTO 用于查询分类(支持分页)
- */
-export class QueryCategoryDTO {
- @ApiProperty({ example: '1', description: '页码' })
+ @ApiProperty({ description: '分类ID', required: false })
@Rule(RuleType.number())
- current: number; // 页码
+ categoryId?: number;
- @ApiProperty({ example: '10', description: '每页大小' })
+ @ApiProperty({ description: '品牌ID', required: false })
@Rule(RuleType.number())
- pageSize: number; // 每页大小
+ brandId?: number;
- @ApiProperty({ example: 'ZYN', description: '关键字' })
+ @ApiProperty({ description: '排序字段', required: false })
@Rule(RuleType.string())
- name: string; // 搜索关键字(支持模糊查询)
+ sortField?: string;
+
+ @ApiProperty({ description: '排序方式', required: false })
+ @Rule(RuleType.string().valid('ascend', 'descend'))
+ sortOrder?: string;
}
-export class CreateFlavorsDTO {
- @ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
- @Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
- name: string;
-
- @Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
- unique_key: string;
-}
-
-export class UpdateFlavorsDTO {
- @ApiProperty({ example: 'ZYN', description: '分类名称' })
- @Rule(RuleType.string())
- name: string;
-}
-
-export class QueryFlavorsDTO {
- @ApiProperty({ example: '1', description: '页码' })
- @Rule(RuleType.number())
- current: number; // 页码
-
- @ApiProperty({ example: '10', description: '每页大小' })
- @Rule(RuleType.number())
- pageSize: number; // 每页大小
-
- @ApiProperty({ example: 'ZYN', description: '关键字' })
- @Rule(RuleType.string())
- name: string; // 搜索关键字(支持模糊查询)
-}
-
-export class CreateStrengthDTO {
- @ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
- @Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
- name: string;
-
- @Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
- unique_key: string;
-}
-
-export class UpdateStrengthDTO {
- @ApiProperty({ example: 'ZYN', description: '分类名称' })
- @Rule(RuleType.string())
- name: string;
-}
-
-export class QueryStrengthDTO {
- @ApiProperty({ example: '1', description: '页码' })
- @Rule(RuleType.number())
- current: number; // 页码
-
- @ApiProperty({ example: '10', description: '每页大小' })
- @Rule(RuleType.number())
- pageSize: number; // 每页大小
-
- @ApiProperty({ example: 'ZYN', description: '关键字' })
- @Rule(RuleType.string())
- name: string; // 搜索关键字(支持模糊查询)
-}
-
-export class SkuItemDTO {
- @ApiProperty({ description: '产品 ID' })
- productId: number;
-
- @ApiProperty({ description: 'sku 编码' })
- sku: string;
-}
-
-export class BatchSetSkuDTO {
- @ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
- skus: SkuItemDTO[];
-}
diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts
index f4aae3c..6180703 100644
--- a/src/dto/reponse.dto.ts
+++ b/src/dto/reponse.dto.ts
@@ -1,5 +1,4 @@
import { ApiProperty } from '@midwayjs/swagger';
-import { Category } from '../entity/category.entity';
import { Order } from '../entity/order.entity';
import { Product } from '../entity/product.entity';
import { StockPoint } from '../entity/stock_point.entity';
@@ -18,12 +17,11 @@ import { Service } from '../entity/service.entity';
import { RateDTO } from './freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.entity';
import { OrderItem } from '../entity/order_item.entity';
-import { OrderRefundItem } from '../entity/order_retund_item.entity';
+import { OrderRefundItem } from '../entity/order_refund_item.entity';
import { OrderNote } from '../entity/order_note.entity';
import { PaymentMethodDTO } from './logistics.dto';
-import { Flavors } from '../entity/flavors.entity';
-import { Strength } from '../entity/strength.entity';
import { Subscription } from '../entity/subscription.entity';
+import { Dict } from '../entity/dict.entity';
export class BooleanRes extends SuccessWrapper(Boolean) {}
//网站配置返回数据
@@ -35,18 +33,49 @@ export class ProductListRes extends SuccessWrapper(ProductPaginatedResponse) {}
//产品返回数据
export class ProductRes extends SuccessWrapper(Product) {}
export class ProductsRes extends SuccessArrayWrapper(Product) {}
-//产品分类返分页数据
-export class CategoryPaginatedResponse extends PaginatedWrapper(Category) {}
-export class FlavorsPaginatedResponse extends PaginatedWrapper(Flavors) {}
-export class StrengthPaginatedResponse extends PaginatedWrapper(Strength) {}
-//产品分类返分页返回数据
-export class ProductCatListRes extends SuccessWrapper(
- CategoryPaginatedResponse
+//产品品牌返分页数据
+export class BrandPaginatedResponse extends PaginatedWrapper(Dict) {}
+//产品品牌返分页返回数据
+export class ProductBrandListRes extends SuccessWrapper(
+ BrandPaginatedResponse
) {}
-//产品分类返所有数据
-export class ProductCatAllRes extends SuccessArrayWrapper(Category) {}
-//产品分类返回数据
-export class ProductCatRes extends SuccessWrapper(Category) {}
+//产品品牌返所有数据
+export class ProductBrandAllRes extends SuccessArrayWrapper(Dict) {}
+//产品品牌返回数据
+export class ProductBrandRes extends SuccessWrapper(Dict) {}
+
+//产品口味返分页数据
+export class FlavorsPaginatedResponse extends PaginatedWrapper(Dict) {}
+//产品口味返分页返回数据
+export class ProductFlavorsListRes extends SuccessWrapper(
+ FlavorsPaginatedResponse
+) {}
+//产品口味返所有数据
+export class ProductFlavorsAllRes extends SuccessArrayWrapper(Dict) {}
+//产品口味返回数据
+export class ProductFlavorsRes extends SuccessWrapper(Dict) {}
+
+//产品规格返分页数据
+export class StrengthPaginatedResponse extends PaginatedWrapper(Dict) {}
+//产品规格返分页返回数据
+export class ProductStrengthListRes extends SuccessWrapper(
+ StrengthPaginatedResponse
+) {}
+//产品规格返所有数据
+export class ProductStrengthAllRes extends SuccessArrayWrapper(Dict) {}
+//产品规格返返回数据
+export class ProductStrengthRes extends SuccessWrapper(Dict) {}
+
+// 产品尺寸返分页数据
+export class SizePaginatedResponse extends PaginatedWrapper(Dict) {}
+// 产品尺寸返分页返回数据
+export class ProductSizeListRes extends SuccessWrapper(
+ SizePaginatedResponse
+) {}
+// 产品尺寸返所有数据
+export class ProductSizeAllRes extends SuccessArrayWrapper(Dict) {}
+// 产品尺寸返回数据
+export class ProductSizeRes extends SuccessWrapper(Dict) {}
//产品分页数据
export class WpProductPaginatedResponse extends PaginatedWrapper(
@@ -119,7 +148,7 @@ export class PaymentMethodListRes extends SuccessArrayWrapper(
PaymentMethodDTO
) {}
-// 订阅分页数据(列表 + 总数等分页信息)
+// 订阅分页数据(列表 + 总数等分页信息)
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
-// 订阅分页返回数据(统一成功包装)
+// 订阅分页返回数据(统一成功包装)
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}
diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts
new file mode 100644
index 0000000..fe63d59
--- /dev/null
+++ b/src/dto/shopyy.dto.ts
@@ -0,0 +1,373 @@
+// Shopyy 平台原始数据类型定义
+// 仅包含当前映射逻辑所需字段以保持简洁与类型安全
+export interface ShopyyTag {
+ id?: number;
+ name?: string;
+}
+// 产品类型
+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?: ShopyyTag[];
+ // 变体列表
+ 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;
+}
+
+// 评论类型
+export interface ShopyyReview {
+ // 主键ID
+ id: number;
+ // 产品ID
+ product_id: number;
+ // 客户ID
+ customer_id: number;
+ // 国家ID
+ country_id: number;
+ // IP地址
+ ip: string;
+ // 评分星级
+ star: number;
+ // 客户名称
+ customer_name: string;
+ // 客户邮箱
+ customer_email: string;
+ // 回复内容
+ reply_content: string;
+ // 评论内容
+ content: string;
+ // 状态 1表示正常
+ status: number;
+ // 是否包含图片 0表示不包含
+ is_image: number;
+ // 图片列表
+ images: any[];
+ // 更新时间戳
+ updated_at: number;
+ // 创建时间戳
+ created_at: number;
+}
+
+
+export interface ShopyyWebhookEvent {
+ id: number;
+ 'event_name': string;
+ 'event_code': string;
+ "event_decript": string;
+ isemail_event: number;
+ email_event_file: string;
+ email_event_status: number;
+ is_webhook: number;
+ is_script_event: number;
+ created_at: number;
+ updated_at: number;
+}
+export interface ShopyyWebhook {
+ id: number;
+ "webhook_name": string;
+ "url": string;
+ event_id: number;
+ event_name: string;
+ event_code: string;
+}
+
+// 发货相关DTO
+export class ShopyyShipOrderItemDTO {
+ order_item_id: number;
+ quantity: number;
+}
+
+export class ShopyyShipOrderDTO {
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: ShopyyShipOrderItemDTO[];
+}
+
+export class ShopyyCancelShipOrderDTO {
+ reason?: string;
+ shipment_id?: string;
+}
+
+export class ShopyyBatchShipOrderItemDTO {
+ order_id: string;
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: ShopyyShipOrderItemDTO[];
+}
+
+export class ShopyyBatchShipOrdersDTO {
+ orders: ShopyyBatchShipOrderItemDTO[];
+}
+
diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts
new file mode 100644
index 0000000..fc9b52a
--- /dev/null
+++ b/src/dto/site-api.dto.ts
@@ -0,0 +1,717 @@
+import { ApiProperty } from '@midwayjs/swagger';
+
+export class UnifiedPaginationDTO {
+ // 分页DTO用于承载统一分页信息与列表数据
+ @ApiProperty({ description: '列表数据' })
+ items: T[];
+
+ @ApiProperty({ description: '总数', example: 100 })
+ total: number;
+
+ @ApiProperty({ description: '当前页', example: 1 })
+ page: number;
+
+ @ApiProperty({ description: '每页数量', example: 20 })
+ per_page: number;
+
+ @ApiProperty({ description: '每页数量别名', example: 20 })
+ page_size?: number;
+
+ @ApiProperty({ description: '总页数', example: 5 })
+ totalPages: number;
+}
+export class UnifiedTagDTO {
+ // 标签DTO用于承载统一标签数据
+ @ApiProperty({ description: '标签ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '标签名称' })
+ name: string;
+}
+export class UnifiedCategoryDTO {
+ // 分类DTO用于承载统一分类数据
+ @ApiProperty({ description: '分类ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '分类名称' })
+ name: string;
+}
+export class UnifiedImageDTO {
+ // 图片DTO用于承载统一图片数据
+ @ApiProperty({ description: '图片ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '图片URL' })
+ src: string;
+
+ @ApiProperty({ description: '图片名称', required: false })
+ name?: string;
+
+ @ApiProperty({ description: '替代文本', required: false })
+ alt?: string;
+}
+
+export class UnifiedAddressDTO {
+ // 地址DTO用于承载统一地址数据
+ @ApiProperty({ description: '名', required: false })
+ first_name?: string;
+
+ @ApiProperty({ description: '姓', required: false })
+ last_name?: string;
+
+ @ApiProperty({ description: '全名', required: false })
+ fullname?: string;
+
+ @ApiProperty({ description: '公司', required: false })
+ company?: string;
+
+ @ApiProperty({ description: '地址1', required: false })
+ address_1?: string;
+
+ @ApiProperty({ description: '地址2', required: false })
+ address_2?: string;
+
+ @ApiProperty({ description: '城市', required: false })
+ city?: string;
+
+ @ApiProperty({ description: '省/州', required: false })
+ state?: string;
+
+ @ApiProperty({ description: '邮政编码', required: false })
+ postcode?: string;
+
+ @ApiProperty({ description: '国家', required: false })
+ country?: string;
+
+ @ApiProperty({ description: '邮箱', required: false })
+ email?: string;
+
+ @ApiProperty({ description: '电话', required: false })
+ phone?: string;
+}
+
+export class UnifiedOrderLineItemDTO {
+ // 订单项DTO用于承载统一订单项数据
+ @ApiProperty({ description: '订单项ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '产品名称' })
+ name: string;
+
+ @ApiProperty({ description: '产品ID' })
+ product_id: number | string;
+
+ @ApiProperty({ description: '变体ID', required: false })
+ variation_id?: number | string;
+
+ @ApiProperty({ description: '数量' })
+ quantity: number;
+
+ @ApiProperty({ description: '总计' })
+ total: string;
+
+ @ApiProperty({ description: 'SKU' })
+ sku: string;
+}
+
+export class UnifiedProductAttributeDTO {
+ // 产品属性DTO用于承载统一产品属性数据
+ @ApiProperty({ description: '属性ID', required: false })
+ id?: number | string;
+
+ @ApiProperty({ description: '属性名称' })
+ name: string;
+
+ @ApiProperty({ description: '属性位置', example: 0, required: false })
+ position?: number;
+
+ @ApiProperty({ description: '对变体是否可见', example: true, required: false })
+ visible?: boolean;
+
+ @ApiProperty({ description: '是否为变体属性', example: true, required: false })
+ variation?: boolean;
+
+ @ApiProperty({ description: '属性选项', type: [String] })
+ options: string[];
+}
+
+export class UnifiedProductVariationDTO {
+ // 产品变体DTO用于承载统一产品变体数据
+ @ApiProperty({ description: '变体ID' })
+ id: number | 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, required: false })
+ image?: UnifiedImageDTO;
+}
+
+export class UnifiedProductDTO {
+ // 产品DTO用于承载统一产品数据
+ @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: () => [UnifiedTagDTO], required: false })
+ tags?: UnifiedTagDTO[];
+
+ @ApiProperty({ description: '产品分类', type: () => [UnifiedCategoryDTO], required: false })
+ categories?: UnifiedCategoryDTO[];
+
+ @ApiProperty({ description: '产品属性', type: () => [UnifiedProductAttributeDTO] })
+ attributes: UnifiedProductAttributeDTO[];
+
+ @ApiProperty({
+ description: '产品变体',
+ type: () => [UnifiedProductVariationDTO],
+ required: false,
+ })
+ variations?: UnifiedProductVariationDTO[];
+
+ @ApiProperty({ description: '创建时间' })
+ date_created: string;
+
+ @ApiProperty({ description: '更新时间' })
+ date_modified: string;
+
+ @ApiProperty({ description: '产品链接', required: false })
+ permalink?: string;
+
+ @ApiProperty({
+ description: '原始数据(保留备用)',
+ type: 'object',
+ required: false,
+ })
+ raw?: Record;
+
+ @ApiProperty({
+ description: 'ERP产品信息',
+ type: 'object',
+ required: false,
+ })
+ erpProduct?: {
+ id: number;
+ sku: string;
+ name: string;
+ nameCn?: string;
+ category?: any;
+ attributes?: any[];
+ components?: any[];
+ price: number;
+ promotionPrice: number;
+ };
+}
+
+export class UnifiedOrderDTO {
+ // 订单DTO用于承载统一订单数据
+ @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: () => [UnifiedOrderLineItemDTO] })
+ line_items: UnifiedOrderLineItemDTO[];
+
+ @ApiProperty({
+ description: '销售项(兼容前端)',
+ type: () => [UnifiedOrderLineItemDTO],
+ required: false,
+ })
+ sales?: UnifiedOrderLineItemDTO[];
+
+ @ApiProperty({ description: '账单地址', type: () => UnifiedAddressDTO })
+ billing: UnifiedAddressDTO;
+
+ @ApiProperty({ description: '收货地址', type: () => UnifiedAddressDTO })
+ shipping: UnifiedAddressDTO;
+
+ @ApiProperty({ description: '账单地址全称', required: false })
+ billing_full_address?: string;
+
+ @ApiProperty({ description: '收货地址全称', required: false })
+ shipping_full_address?: string;
+
+ @ApiProperty({ description: '支付方式' })
+ payment_method: string;
+ refunds: UnifiedOrderRefundDTO[];
+ @ApiProperty({ description: '创建时间' })
+ date_created: string;
+
+ @ApiProperty({ description: '更新时间', required: false })
+ date_modified?: string;
+
+ @ApiProperty({ description: '原始数据', type: 'object', required: false })
+ raw?: Record;
+}
+
+export class UnifiedCustomerDTO {
+ // 客户DTO用于承载统一客户数据
+ @ApiProperty({ description: '客户ID' })
+ id: string | number;
+
+ @ApiProperty({ description: '头像URL', required: false })
+ avatar?: string;
+
+ @ApiProperty({ description: '邮箱' })
+ email: string;
+
+ @ApiProperty({ description: '订单总数', required: false })
+ orders?: number;
+
+ @ApiProperty({ description: '总花费', required: false })
+ total_spend?: number;
+
+ @ApiProperty({ description: '创建时间', required: false })
+ date_created?: string;
+
+ @ApiProperty({ description: '更新时间', required: false })
+ date_modified?: string;
+
+ @ApiProperty({ description: '名', required: false })
+ first_name?: string;
+
+ @ApiProperty({ description: '姓', required: false })
+ last_name?: string;
+
+ @ApiProperty({ description: '名字', required: false })
+ fullname?: string;
+
+ @ApiProperty({ description: '用户名', required: false })
+ username?: string;
+
+ @ApiProperty({ description: '电话', required: false })
+ phone?: string;
+
+ @ApiProperty({
+ description: '账单地址',
+ type: () => UnifiedAddressDTO,
+ required: false,
+ })
+ billing?: UnifiedAddressDTO;
+
+ @ApiProperty({
+ description: '收货地址',
+ type: () => UnifiedAddressDTO,
+ required: false,
+ })
+ shipping?: UnifiedAddressDTO;
+
+ @ApiProperty({ description: '原始数据', type: 'object', required: false })
+ raw?: Record;
+}
+
+export class UnifiedSubscriptionDTO {
+ // 订阅DTO用于承载统一订阅数据
+ @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: '创建时间', required: false })
+ date_created?: string;
+
+ @ApiProperty({ description: '更新时间', required: false })
+ date_modified?: string;
+
+ @ApiProperty({ description: '开始时间' })
+ start_date: string;
+
+ @ApiProperty({ description: '下次支付时间' })
+ next_payment_date: string;
+
+ @ApiProperty({ description: '订单项', type: () => [UnifiedOrderLineItemDTO] })
+ line_items: UnifiedOrderLineItemDTO[];
+
+ @ApiProperty({ description: '原始数据', type: 'object', required: false })
+ raw?: Record;
+}
+
+export class UnifiedMediaDTO {
+ // 媒体DTO用于承载统一媒体数据
+ @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;
+
+ @ApiProperty({ description: '更新时间', required: false })
+ 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 UnifiedReviewDTO {
+ // 评论DTO用于承载统一评论数据
+ @ApiProperty({ description: '评论ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '产品ID' })
+ product_id: number | string;
+
+ @ApiProperty({ description: '评论者' })
+ author: string;
+
+ @ApiProperty({ description: '评论者邮箱' })
+ email: string;
+
+ @ApiProperty({ description: '评论内容' })
+ content: string;
+
+ @ApiProperty({ description: '评分' })
+ rating: number;
+
+ @ApiProperty({ description: '状态' })
+ status: string;
+
+ @ApiProperty({ description: '创建时间' })
+ date_created: string;
+
+ @ApiProperty({ description: '更新时间', required: false })
+ date_modified?: string;
+
+ @ApiProperty({ description: '原始数据', type: 'object', required: false })
+ raw?: Record;
+}
+
+export class UnifiedReviewPaginationDTO extends UnifiedPaginationDTO {
+ // 评论分页DTO用于承载评论列表分页数据
+ @ApiProperty({ description: '列表数据', type: () => [UnifiedReviewDTO] })
+ items: UnifiedReviewDTO[];
+}
+
+export class CreateReviewDTO {
+ @ApiProperty({ description: '产品ID' })
+ product_id: number | string;
+
+ @ApiProperty({ description: '评论内容' })
+ review: string;
+
+ @ApiProperty({ description: '评论者' })
+ reviewer: string;
+
+ @ApiProperty({ description: '评论者邮箱' })
+ reviewer_email: string;
+
+ @ApiProperty({ description: '评分' })
+ rating: number;
+}
+
+export class UpdateReviewDTO {
+ @ApiProperty({ description: '评论内容', required: false })
+ review?: string;
+
+ @ApiProperty({ description: '评分', required: false })
+ rating?: number;
+
+ @ApiProperty({ description: '状态', required: false })
+ status?: string;
+}
+
+export class UploadMediaDTO {
+ @ApiProperty({ description: 'Base64 编码的文件内容' })
+ file: string;
+
+ @ApiProperty({ description: '文件名' })
+ filename: string;
+}
+
+export class UnifiedSearchParamsDTO {
+ // 统一查询参数DTO用于承载分页与筛选与排序参数
+ @ApiProperty({ description: '页码', example: 1, required: false })
+ page?: number;
+
+ @ApiProperty({ description: '每页数量', example: 20, required: false })
+ per_page?: number;
+
+ @ApiProperty({ description: '每页数量别名', example: 20, required: false })
+ page_size?: number;
+
+ @ApiProperty({ description: '搜索关键词', required: false })
+ search?: string;
+
+ @ApiProperty({ description: '状态', required: false })
+ status?: string;
+
+ @ApiProperty({ description: '客户ID,用于筛选订单', required: false })
+ customer_id?: number;
+
+ @ApiProperty({ description: '过滤条件对象', type: 'object', required: false })
+ where?: Record;
+
+ @ApiProperty({
+ description: '排序对象,例如 { "sku": "desc" }',
+ type: 'object',
+ required: false,
+ })
+ order?: Record | string;
+
+ @ApiProperty({ description: '排序字段(兼容旧入参)', required: false })
+ orderby?: string;
+
+ @ApiProperty({ description: '排序方式(兼容旧入参)', required: false })
+ orderDir?: 'asc' | 'desc';
+
+ @ApiProperty({ description: '选中ID列表,逗号分隔', required: false })
+ ids?: string;
+}
+
+export class UnifiedWebhookDTO {
+ // Webhook DTO用于承载统一webhook数据
+ @ApiProperty({ description: 'Webhook ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '名称' })
+ name?: string;
+
+ @ApiProperty({ description: '状态' })
+ status: string;
+
+ @ApiProperty({ description: '主题/事件' })
+ topic: string;
+
+ @ApiProperty({ description: '目标URL' })
+ delivery_url: string;
+
+ @ApiProperty({ description: '秘密密钥' })
+ secret?: string;
+
+ @ApiProperty({ description: '创建时间' })
+ date_created?: string;
+
+ @ApiProperty({ description: '更新时间' })
+ date_modified?: string;
+
+ @ApiProperty({ description: '头部信息' })
+ headers?: Record;
+
+ @ApiProperty({ description: 'API版本' })
+ api_version?: string;
+}
+
+export class UnifiedWebhookPaginationDTO extends UnifiedPaginationDTO {
+ // Webhook分页DTO用于承载webhook列表分页数据
+ @ApiProperty({ description: '列表数据', type: () => [UnifiedWebhookDTO] })
+ items: UnifiedWebhookDTO[];
+}
+
+export class CreateWebhookDTO {
+ // 创建Webhook DTO
+ @ApiProperty({ description: '名称' })
+ name?: string;
+
+ @ApiProperty({ description: '主题/事件' })
+ topic: string;
+
+ @ApiProperty({ description: '目标URL' })
+ delivery_url: string;
+
+ @ApiProperty({ description: '秘密密钥' })
+ secret?: string;
+
+ @ApiProperty({ description: '头部信息' })
+ headers?: Record;
+
+ @ApiProperty({ description: 'API版本' })
+ api_version?: string;
+}
+
+export class UpdateWebhookDTO {
+ // 更新Webhook DTO
+ @ApiProperty({ description: '名称', required: false })
+ name?: string;
+
+ @ApiProperty({ description: '状态', required: false })
+ status?: string;
+
+ @ApiProperty({ description: '主题/事件', required: false })
+ topic?: string;
+
+ @ApiProperty({ description: '目标URL', required: false })
+ delivery_url?: string;
+
+ @ApiProperty({ description: '秘密密钥', required: false })
+ secret?: string;
+
+ @ApiProperty({ description: '头部信息', required: false })
+ headers?: Record;
+
+ @ApiProperty({ description: 'API版本', required: false })
+ api_version?: string;
+}
+
+export class UnifiedOrderRefundDTO {
+ @ApiProperty({ description: '退款ID' })
+ id: number | string;
+
+ @ApiProperty({ description: '退款原因' })
+ reason: string;
+
+ @ApiProperty({ description: '退款金额' })
+ total: string;
+}
+
+export class ShipOrderItemDTO {
+ @ApiProperty({ description: '订单项ID' })
+ order_item_id: number;
+
+ @ApiProperty({ description: '数量' })
+ quantity: number;
+}
+
+export class ShipOrderDTO {
+ @ApiProperty({ description: '物流单号', required: false })
+ tracking_number?: string;
+
+ @ApiProperty({ description: '物流公司', required: false })
+ shipping_provider?: string;
+
+ @ApiProperty({ description: '发货方式', required: false })
+ shipping_method?: string;
+
+ @ApiProperty({ description: '发货商品项', type: () => [ShipOrderItemDTO], required: false })
+ items?: ShipOrderItemDTO[];
+}
+
+export class CancelShipOrderDTO {
+ @ApiProperty({ description: '取消原因', required: false })
+ reason?: string;
+
+ @ApiProperty({ description: '发货单ID', required: false })
+ shipment_id?: string;
+}
+
+export class BatchShipOrderItemDTO {
+ @ApiProperty({ description: '订单ID' })
+ order_id: string;
+
+ @ApiProperty({ description: '物流单号', required: false })
+ tracking_number?: string;
+
+ @ApiProperty({ description: '物流公司', required: false })
+ shipping_provider?: string;
+
+ @ApiProperty({ description: '发货方式', required: false })
+ shipping_method?: string;
+
+ @ApiProperty({ description: '发货商品项', type: () => [ShipOrderItemDTO], required: false })
+ items?: ShipOrderItemDTO[];
+}
+
+export class BatchShipOrdersDTO {
+ @ApiProperty({ description: '批量发货订单列表', type: () => [BatchShipOrderItemDTO] })
+ orders: BatchShipOrderItemDTO[];
+}
\ No newline at end of file
diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts
index 17fcdd1..b45a3e3 100644
--- a/src/dto/site.dto.ts
+++ b/src/dto/site.dto.ts
@@ -20,7 +20,11 @@ export class SiteConfig {
@ApiProperty({ description: '站点名' })
@Rule(RuleType.string())
- siteName: string;
+ name: string;
+
+ @ApiProperty({ description: '描述' })
+ @Rule(RuleType.string().allow('').optional())
+ description?: string;
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
@@ -35,15 +39,31 @@ export class CreateSiteDTO {
@Rule(RuleType.string().optional())
apiUrl?: string;
@Rule(RuleType.string().optional())
+ websiteUrl?: string;
+ @Rule(RuleType.string().optional())
consumerKey?: string;
@Rule(RuleType.string().optional())
consumerSecret?: string;
+ @Rule(RuleType.string().optional())
+ token?: string;
@Rule(RuleType.string())
- siteName: string;
+ name: string;
+ @Rule(RuleType.string().allow('').optional())
+ description?: string;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
skuPrefix?: string;
+
+ // 区域
+ @ApiProperty({ description: '区域' })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ areas?: string[];
+
+ // 绑定仓库
+ @ApiProperty({ description: '绑定仓库ID列表' })
+ @Rule(RuleType.array().items(RuleType.number()).optional())
+ stockPointIds?: number[];
}
export class UpdateSiteDTO {
@@ -54,13 +74,30 @@ export class UpdateSiteDTO {
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string().optional())
- siteName?: string;
+ token?: string;
+ @Rule(RuleType.string().optional())
+ name?: string;
+ @Rule(RuleType.string().allow('').optional())
+ description?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
skuPrefix?: string;
+
+ // 区域
+ @ApiProperty({ description: '区域' })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ areas?: string[];
+
+ // 绑定仓库
+ @ApiProperty({ description: '绑定仓库ID列表' })
+ @Rule(RuleType.array().items(RuleType.number()).optional())
+ stockPointIds?: number[];
+ @ApiProperty({ description: '站点网站URL' })
+ @Rule(RuleType.string().optional())
+ websiteUrl?: string;
}
export class QuerySiteDTO {
diff --git a/src/dto/statistics.dto.ts b/src/dto/statistics.dto.ts
index b6cf9dc..31e10a8 100644
--- a/src/dto/statistics.dto.ts
+++ b/src/dto/statistics.dto.ts
@@ -33,8 +33,4 @@ export class OrderStatisticsParams {
@ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' })
@Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt'))
brand: string;
-
- @ApiProperty({ enum: ['day', 'week', 'month'], default: 'day' })
- @Rule(RuleType.string().valid('day', 'week', 'month'))
- grouping: string;
}
diff --git a/src/dto/stock.dto.ts b/src/dto/stock.dto.ts
index 24cfdd8..54629a0 100644
--- a/src/dto/stock.dto.ts
+++ b/src/dto/stock.dto.ts
@@ -20,7 +20,19 @@ export class QueryStockDTO {
@ApiProperty()
@Rule(RuleType.string())
- productName: string;
+ name: string;
+
+ @ApiProperty()
+ @Rule(RuleType.string())
+ sku: string;
+
+ @ApiProperty({ description: '按库存点ID排序', required: false })
+ @Rule(RuleType.number().allow(null))
+ sortPointId?: number;
+
+ @ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }', required: false })
+ @Rule(RuleType.object().allow(null))
+ order?: Record;
}
export class QueryPointDTO {
@ApiProperty({ example: '1', description: '页码' })
@@ -46,11 +58,11 @@ export class QueryStockRecordDTO {
@ApiProperty()
@Rule(RuleType.string())
- productSku: string;
+ sku: string;
@ApiProperty()
@Rule(RuleType.string())
- productName: string;
+ name: string;
@ApiProperty()
@Rule(RuleType.string())
@@ -84,7 +96,7 @@ export class QueryPurchaseOrderDTO {
export class StockDTO extends Stock {
@ApiProperty()
@Rule(RuleType.string())
- productName: string;
+ name: string;
@ApiProperty({
type: 'object',
@@ -120,7 +132,7 @@ export class UpdateStockDTO {
@ApiProperty()
@Rule(RuleType.string())
- productSku: string;
+ sku: string;
@ApiProperty()
@Rule(RuleType.number())
@@ -155,6 +167,19 @@ export class CreateStockPointDTO {
@ApiProperty()
@Rule(RuleType.string())
contactPhone: string;
+
+ // 区域
+ @ApiProperty({ description: '区域' })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ areas?: string[];
+
+ @ApiProperty({ description: '上游仓库点ID' })
+ @Rule(RuleType.number().optional())
+ upStreamStockPointId?: number;
+
+ @ApiProperty({ description: '上游名称' })
+ @Rule(RuleType.string().optional())
+ upStreamName?: string;
}
export class UpdateStockPointDTO extends CreateStockPointDTO {}
diff --git a/src/dto/subscription.dto.ts b/src/dto/subscription.dto.ts
index 79794db..4276ec1 100644
--- a/src/dto/subscription.dto.ts
+++ b/src/dto/subscription.dto.ts
@@ -2,9 +2,9 @@ import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import { SubscriptionStatus } from '../enums/base.enum';
-// 订阅列表查询参数(分页与筛选)
+// 订阅列表查询参数(分页与筛选)
export class QuerySubscriptionDTO {
- // 当前页码(从 1 开始)
+ // 当前页码(从 1 开始)
@ApiProperty({ example: 1, description: '页码' })
@Rule(RuleType.number().default(1))
current: number;
@@ -14,23 +14,23 @@ export class QuerySubscriptionDTO {
@Rule(RuleType.number().default(10))
pageSize: number;
- // 站点 ID(可选)
+ // 站点 ID(可选)
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.string().allow(''))
siteId: string;
- // 订阅状态筛选(可选),支持枚举值
+ // 订阅状态筛选(可选),支持枚举值
@ApiProperty({ description: '订阅状态', enum: SubscriptionStatus })
@Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow(''))
status: SubscriptionStatus | '';
- // 客户邮箱(模糊匹配,可选)
+ // 客户邮箱(模糊匹配,可选)
@ApiProperty({ description: '客户邮箱' })
@Rule(RuleType.string().allow(''))
customer_email: string;
- // 关键字(订阅ID、邮箱等,模糊匹配,可选)
- @ApiProperty({ description: '关键字(订阅ID、邮箱等)' })
+ // 关键字(订阅ID,邮箱等,模糊匹配,可选)
+ @ApiProperty({ description: '关键字(订阅ID,邮箱等)' })
@Rule(RuleType.string().allow(''))
keyword: string;
}
\ No newline at end of file
diff --git a/src/dto/template.dto.ts b/src/dto/template.dto.ts
new file mode 100644
index 0000000..51ff636
--- /dev/null
+++ b/src/dto/template.dto.ts
@@ -0,0 +1,40 @@
+import { ApiProperty } from '@midwayjs/swagger';
+import { Rule, RuleType } from '@midwayjs/validate';
+
+export class CreateTemplateDTO {
+ @ApiProperty({ description: '模板名称', required: true })
+ @Rule(RuleType.string().required())
+ name: string;
+
+ @ApiProperty({ description: '模板内容', required: true })
+ @Rule(RuleType.string().required())
+ value: string;
+
+ @ApiProperty({ description: '测试数据JSON', required: false })
+ @Rule(RuleType.string().optional())
+ testData?: string;
+}
+
+export class UpdateTemplateDTO {
+ @ApiProperty({ description: '模板名称', required: true })
+ @Rule(RuleType.string().required())
+ name: string;
+
+ @ApiProperty({ description: '模板内容', required: true })
+ @Rule(RuleType.string().required())
+ value: string;
+
+ @ApiProperty({ description: '测试数据JSON', required: false })
+ @Rule(RuleType.string().optional())
+ testData?: string;
+}
+
+export class RenderTemplateDTO {
+ @ApiProperty({ description: '模板内容', required: true })
+ @Rule(RuleType.string().required())
+ template: string;
+
+ @ApiProperty({ description: '渲染数据', required: true })
+ @Rule(RuleType.object().required())
+ data: Record;
+}
diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts
new file mode 100644
index 0000000..044afe1
--- /dev/null
+++ b/src/dto/woocommerce.dto.ts
@@ -0,0 +1,554 @@
+// 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;
+ // 支付方式标题
+ 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 WooOrderRefund {
+ id?: number;
+ reason?: string;
+ total?: 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 WooReview {
+ // 评论ID
+ id: number;
+ // 评论内容
+ review: string;
+ // 评分
+ rating: number;
+ // 评论者
+ reviewer: string;
+ // 评论者邮箱
+ reviewer_email: string;
+ // 状态
+ status: string;
+ // 产品ID
+ product_id: number;
+ // 创建日期
+ date_created: 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;
+}
+
+// Webhook类型
+export interface WooWebhook {
+ id: number;
+ name: string;
+ status: string;
+ topic: string;
+ resource: string;
+ event: string;
+ hooks: string;
+ delivery_url: string;
+ secret: string;
+ date_created: string;
+ date_created_gmt: string;
+ date_modified: string;
+ date_modified_gmt: string;
+ api_version: string;
+ meta_data?: Array<{ id?: number; key: string; value: any }>;
+}
+
+
+
+export interface WooOrderSearchParams {
+ context: WooContext;
+ page: number;
+ per_page: number;
+ search: string;
+ after: string;
+ before: string;
+ modified_after: string;
+ modified_before: string;
+ date_are_gmt: boolean;
+ exclude: string[];
+ include: string[];
+ offset: number;
+ order: string;
+ orderby: string;
+ parant: string[];
+ status: (WooOrderStatusSearchParams)[];
+ customer: number;
+ product: number;
+ dp: number;
+ created_via: string;
+}
+
+
+export enum WooOrderStatusSearchParams {
+ pending,
+ processing,
+ "on-hold",
+ completed,
+ cancelled,
+ refunded,
+ failed,
+ trash,
+ any
+}
+
+
+export interface WooProductSearchParams extends ListParams {
+ slug: string;
+ status: string[];
+ include_status: string;
+ exclude_status: string;
+ type: string;
+ include_types: string;
+ exclude_types: string;
+ sku: string;
+ featured: boolean;
+ category: string;
+ tag: string;
+ shipping_class: string;
+ attribute: string;
+ attribute_term: string;
+ tax_class: string;
+ on_sale: boolean;
+ min_price: string;
+ max_price: string;
+ stock_status: string;
+ virtual: boolean;
+ downloadable: boolean;
+}
+
+export interface ListParams {
+ context: WooContext;
+ page: number;
+ per_page: number;
+ search: string;
+ search_fields: any[];
+ after: string;
+ before: string;
+ modified_after: string;
+ modified_before: string;
+ date_are_gmt: boolean;
+ exclude: string[];
+ include: string[];
+ offset: number;
+ order: string;
+ orderby: string;
+ parant: string[];
+ parent_exclude: string[];
+}
+export enum WooContext {
+ view,
+ edit
+}
+export enum WooProductStatusSearchParams {
+ any,
+ draft,
+ pending,
+ private,
+ publish
+}
+
+// 发货相关DTO
+export class WooShipOrderItemDTO {
+ order_item_id: number;
+ quantity: number;
+}
+
+export class WooShipOrderDTO {
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: WooShipOrderItemDTO[];
+}
+
+export class WooCancelShipOrderDTO {
+ reason?: string;
+ shipment_id?: string;
+}
+
+export class WooBatchShipOrderItemDTO {
+ order_id: string;
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: WooShipOrderItemDTO[];
+}
+
+export class WooBatchShipOrdersDTO {
+ orders: WooBatchShipOrderItemDTO[];
+}
\ No newline at end of file
diff --git a/src/dto/wp_product.dto.ts b/src/dto/wp_product.dto.ts
index 11e2a7b..48212a3 100644
--- a/src/dto/wp_product.dto.ts
+++ b/src/dto/wp_product.dto.ts
@@ -13,46 +13,58 @@ export class WpProductDTO extends WpProduct {
export class UpdateVariationDTO {
@ApiProperty({ description: '产品名称' })
- @Rule(RuleType.string())
- name: string;
+ @Rule(RuleType.string().optional())
+ name?: string;
@ApiProperty({ description: 'SKU' })
- @Rule(RuleType.string().allow(''))
- sku: string;
+ @Rule(RuleType.string().allow('').optional())
+ sku?: string;
@ApiProperty({ description: '常规价格', type: Number })
- @Rule(RuleType.number())
- regular_price: number; // 常规价格
+ @Rule(RuleType.number().optional())
+ regular_price?: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
- @Rule(RuleType.number())
- sale_price: number; // 销售价格
+ @Rule(RuleType.number().optional())
+ sale_price?: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
- @Rule(RuleType.boolean())
- on_sale: boolean; // 是否促销中
+ @Rule(RuleType.boolean().optional())
+ on_sale?: boolean; // 是否促销中
}
export class UpdateWpProductDTO {
@ApiProperty({ description: '变体名称' })
- @Rule(RuleType.string())
- name: string;
+ @Rule(RuleType.string().optional())
+ name?: string;
@ApiProperty({ description: 'SKU' })
- @Rule(RuleType.string().allow(''))
- sku: string;
+ @Rule(RuleType.string().allow('').optional())
+ sku?: string;
@ApiProperty({ description: '常规价格', type: Number })
- @Rule(RuleType.number())
- regular_price: number; // 常规价格
+ @Rule(RuleType.number().optional())
+ regular_price?: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
- @Rule(RuleType.number())
- sale_price: number; // 销售价格
+ @Rule(RuleType.number().optional())
+ sale_price?: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
- @Rule(RuleType.boolean())
- on_sale: boolean; // 是否促销中
+ @Rule(RuleType.boolean().optional())
+ on_sale?: boolean; // 是否促销中
+
+ @ApiProperty({ description: '分类列表', type: [String] })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ categories?: string[];
+
+ @ApiProperty({ description: '标签列表', type: [String] })
+ @Rule(RuleType.array().items(RuleType.string()).optional())
+ tags?: string[];
+
+ @ApiProperty({ description: '站点ID', required: false })
+ @Rule(RuleType.number().optional())
+ siteId?: number;
}
export class QueryWpProductDTO {
@@ -75,24 +87,50 @@ export class QueryWpProductDTO {
@ApiProperty({ description: '产品状态', enum: ProductStatus })
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
status?: ProductStatus;
+
+ @ApiProperty({ description: 'SKU列表', type: Array })
+ @Rule(RuleType.array().items(RuleType.string()).single())
+ skus?: string[];
}
-export class SetConstitutionDTO {
- @ApiProperty({ type: Boolean })
- @Rule(RuleType.boolean())
- isProduct: boolean;
-
- @ApiProperty({
- description: '构成成分',
- type: 'array',
- items: {
- type: 'object',
- properties: {
- sku: { type: 'string' },
- quantity: { type: 'number' },
- },
- },
- })
- @Rule(RuleType.array())
- constitution: { sku: string; quantity: number }[] | null;
+export class BatchSyncProductsDTO {
+ @ApiProperty({ description: '产品ID列表', type: [Number] })
+ @Rule(RuleType.array().items(RuleType.number()).required())
+ productIds: number[];
+}
+
+export class BatchUpdateTagsDTO {
+ @ApiProperty({ description: '产品ID列表', type: [Number] })
+ @Rule(RuleType.array().items(RuleType.number()).required())
+ ids: number[];
+
+ @ApiProperty({ description: '标签列表', type: [String] })
+ @Rule(RuleType.array().items(RuleType.string()).required())
+ tags: string[];
+}
+
+export class BatchUpdateProductsDTO {
+ @ApiProperty({ description: '产品ID列表', type: [Number] })
+ @Rule(RuleType.array().items(RuleType.number()).required())
+ ids: number[];
+
+ @ApiProperty({ description: '常规价格', type: Number })
+ @Rule(RuleType.number())
+ regular_price?: number;
+
+ @ApiProperty({ description: '销售价格', type: Number })
+ @Rule(RuleType.number())
+ sale_price?: number;
+
+ @ApiProperty({ description: '分类列表', type: [String] })
+ @Rule(RuleType.array().items(RuleType.string()))
+ categories?: string[];
+
+ @ApiProperty({ description: '标签列表', type: [String] })
+ @Rule(RuleType.array().items(RuleType.string()))
+ tags?: string[];
+
+ @ApiProperty({ description: '状态', enum: ProductStatus })
+ @Rule(RuleType.string().valid(...Object.values(ProductStatus)))
+ status?: ProductStatus;
}
diff --git a/src/entity/area.entity.ts b/src/entity/area.entity.ts
new file mode 100644
index 0000000..e66de47
--- /dev/null
+++ b/src/entity/area.entity.ts
@@ -0,0 +1,17 @@
+
+import { ApiProperty } from '@midwayjs/swagger';
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+
+@Entity('area')
+export class Area {
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @ApiProperty({ description: '名称' })
+ @Column()
+ name: string;
+
+ @ApiProperty({ description: '编码' })
+ @Column({ unique: true })
+ code: string;
+}
diff --git a/src/entity/category.entity.ts b/src/entity/category.entity.ts
index 2e52e8a..c0c86ae 100644
--- a/src/entity/category.entity.ts
+++ b/src/entity/category.entity.ts
@@ -1,53 +1,39 @@
-import {
- PrimaryGeneratedColumn,
- Column,
- CreateDateColumn,
- UpdateDateColumn,
- Entity,
-} from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
+import { Product } from './product.entity';
+import { CategoryAttribute } from './category_attribute.entity';
-@Entity()
+@Entity('category')
export class Category {
- @ApiProperty({
- example: '1',
- description: '分类 ID',
- type: 'number',
- required: true,
- })
+ @ApiProperty({ description: 'ID' })
@PrimaryGeneratedColumn()
id: number;
- @ApiProperty({
- example: '分类名称',
- description: '分类名称',
- type: 'string',
- required: true,
- })
+ @ApiProperty({ description: '分类显示名称' })
@Column()
+ title: string;
+
+ @ApiProperty({ description: '分类中文名称' })
+ @Column({ nullable: true })
+ titleCN: string;
+
+ @ApiProperty({ description: '分类唯一标识' })
+ @Column({ unique: true })
name: string;
- @ApiProperty({
- description: '唯一识别key',
- type: 'string',
- required: true,
- })
- @Column()
- unique_key: string;
+ @ApiProperty({ description: '排序' })
+ @Column({ default: 0 })
+ sort: number;
+
+ @OneToMany(() => Product, product => product.category)
+ products: Product[];
+
+ @OneToMany(() => CategoryAttribute, categoryAttribute => categoryAttribute.category)
+ attributes: CategoryAttribute[];
- @ApiProperty({
- example: '2022-12-12 11:11:11',
- description: '创建时间',
- required: true,
- })
@CreateDateColumn()
createdAt: Date;
- @ApiProperty({
- example: '2022-12-12 11:11:11',
- description: '更新时间',
- required: true,
- })
@UpdateDateColumn()
updatedAt: Date;
}
diff --git a/src/entity/category_attribute.entity.ts b/src/entity/category_attribute.entity.ts
new file mode 100644
index 0000000..830c087
--- /dev/null
+++ b/src/entity/category_attribute.entity.ts
@@ -0,0 +1,26 @@
+import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Category } from './category.entity';
+import { Dict } from './dict.entity';
+import { ApiProperty } from '@midwayjs/swagger';
+
+@Entity()
+export class CategoryAttribute {
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @ApiProperty({ description: '分类' })
+ @ManyToOne(() => Category, { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'category_id' })
+ category: Category;
+
+ @ApiProperty({ description: '关联的属性字典' })
+ @ManyToOne(() => Dict, { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'attribute_dict_id' })
+ attributeDict: Dict;
+
+ @CreateDateColumn()
+ createdAt: Date;
+
+ @UpdateDateColumn()
+ updatedAt: Date;
+}
diff --git a/src/entity/dict.entity.ts b/src/entity/dict.entity.ts
new file mode 100644
index 0000000..dc0d9af
--- /dev/null
+++ b/src/entity/dict.entity.ts
@@ -0,0 +1,42 @@
+/**
+ * @description 字典
+ * @author ZKS
+ * @date 2025-11-27
+ */
+import { DictItem } from './dict_item.entity';
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ OneToMany,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+
+@Entity()
+export class Dict {
+ // 主键
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @Column({comment: '字典显示名称'})
+ title: string;
+ // 字典名称
+ @Column({ unique: true, comment: '字典名称' })
+ name: string;
+
+ // 字典项
+ @OneToMany(() => DictItem, item => item.dict)
+ items: DictItem[];
+
+ // 是否可删除
+ @Column({ default: true, comment: '是否可删除' })
+ deletable: boolean;
+ // 创建时间
+ @CreateDateColumn()
+ createdAt: Date;
+
+ // 更新时间
+ @UpdateDateColumn()
+ updatedAt: Date;
+}
diff --git a/src/entity/dict_item.entity.ts b/src/entity/dict_item.entity.ts
new file mode 100644
index 0000000..ffa6896
--- /dev/null
+++ b/src/entity/dict_item.entity.ts
@@ -0,0 +1,67 @@
+/**
+ * @description 字典项
+ * @author ZKS
+ * @date 2025-11-27
+ */
+import { Dict } from './dict.entity';
+import { Product } from './product.entity';
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ Index,
+ JoinColumn,
+ ManyToMany,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+
+@Entity()
+@Index(['name', 'dict'], { unique: true })
+export class DictItem {
+ // 主键
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ // 字典项名称
+ @Column({ comment: '字典项显示名称' })
+ title: string;
+ // 目前没有单独做国际化, 所以这里先添加 titleCN 用来标注
+ @Column({ comment: '字典项中文名称', nullable: true })
+ titleCN: string;
+ // 唯一标识
+ @Column({ comment: '字典唯一标识名称' })
+ name: string;
+
+ // 字典项值
+ @Column({ nullable: true, comment: '字典项值' })
+ value?: string;
+
+ @Column({ nullable: true, comment: '图片' })
+ image: string;
+
+ @Column({ nullable: true, comment: '简称' })
+ shortName: string;
+
+ // 排序
+ @Column({ default: 0, comment: '排序' })
+ sort: number;
+
+ // 属于哪个字典
+ @ManyToOne(() => Dict, dict => dict.items)
+ @JoinColumn({ name: 'dict_id' })
+ dict: Dict;
+
+ // 关联的产品
+ @ManyToMany(() => Product, product => product.attributes)
+ products: Product[];
+
+ // 创建时间
+ @CreateDateColumn()
+ createdAt: Date;
+
+ // 更新时间
+ @UpdateDateColumn()
+ updatedAt: Date;
+}
diff --git a/src/entity/flavors.entity.ts b/src/entity/flavors.entity.ts
deleted file mode 100644
index 9b0cb10..0000000
--- a/src/entity/flavors.entity.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import {
- PrimaryGeneratedColumn,
- Column,
- CreateDateColumn,
- UpdateDateColumn,
- Entity,
-} from 'typeorm';
-import { ApiProperty } from '@midwayjs/swagger';
-
-@Entity()
-export class Flavors {
- @ApiProperty()
- @PrimaryGeneratedColumn()
- id: number;
-
- @ApiProperty()
- @Column()
- name: string;
-
- @ApiProperty({
- description: '唯一识别key',
- type: 'string',
- required: true,
- })
- @Column()
- unique_key: string;
-
- @ApiProperty({
- example: '2022-12-12 11:11:11',
- description: '创建时间',
- required: true,
- })
- @CreateDateColumn()
- createdAt: Date;
-
- @ApiProperty({
- example: '2022-12-12 11:11:11',
- description: '更新时间',
- required: true,
- })
- @UpdateDateColumn()
- updatedAt: Date;
-}
diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts
index fc51dd2..09fd126 100644
--- a/src/entity/order.entity.ts
+++ b/src/entity/order.entity.ts
@@ -24,9 +24,9 @@ export class Order {
id: number;
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 来源站点唯一标识
@ApiProperty()
@Column()
@@ -178,7 +178,7 @@ export class Order {
@ApiProperty()
@Column({
type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT
- nullable: true, // 可选:是否允许为 NULL
+ nullable: true, // 可选:是否允许为 NULL
})
@Expose()
customer_note: string;
diff --git a/src/entity/order_copon.entity.ts b/src/entity/order_coupon.entity.ts
similarity index 95%
rename from src/entity/order_copon.entity.ts
rename to src/entity/order_coupon.entity.ts
index c1a16bd..0670870 100644
--- a/src/entity/order_copon.entity.ts
+++ b/src/entity/order_coupon.entity.ts
@@ -22,9 +22,9 @@ export class OrderCoupon {
orderId: number; // 订单 ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 站点ID
@ApiProperty()
@Column()
diff --git a/src/entity/order_fee.entity.ts b/src/entity/order_fee.entity.ts
index 5b83850..b7d8888 100644
--- a/src/entity/order_fee.entity.ts
+++ b/src/entity/order_fee.entity.ts
@@ -22,9 +22,9 @@ export class OrderFee {
orderId: number; // 订单 ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string;
+ siteId: number; // 站点ID
@ApiProperty()
@Column()
diff --git a/src/entity/order_item.entity.ts b/src/entity/order_item.entity.ts
index 0a3fd5b..dca5c62 100644
--- a/src/entity/order_item.entity.ts
+++ b/src/entity/order_item.entity.ts
@@ -22,9 +22,9 @@ export class OrderItem {
name: string;
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 来源站点唯一标识
@ApiProperty()
@Column()
@@ -79,17 +79,17 @@ export class OrderItem {
@ApiProperty()
@Column({ nullable: true })
@Expose()
- tax_class?: string; // 税类(来自 line_items.tax_class)
+ tax_class?: string; // 税类(来自 line_items.tax_class)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
- taxes?: any[]; // 税明细(来自 line_items.taxes,数组)
+ taxes?: any[]; // 税明细(来自 line_items.taxes,数组)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
- meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
+ meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
@ApiProperty()
@Column({ nullable: true })
@@ -99,7 +99,7 @@ export class OrderItem {
@ApiProperty()
@Column({ nullable: true })
@Expose()
- global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供)
+ global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供)
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@@ -109,17 +109,17 @@ export class OrderItem {
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
- image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src)
+ image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src)
@ApiProperty()
@Column({ nullable: true })
@Expose()
- parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
+ parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
@ApiProperty()
@Column({ nullable: true })
@Expose()
- bundled_by?: string; // 捆绑来源标识(bundled_by)
+ bundled_by?: string; // 捆绑来源标识(bundled_by)
@ApiProperty()
@Column({ nullable: true })
@@ -129,7 +129,7 @@ export class OrderItem {
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
- bundled_items?: any[]; // 捆绑项列表(数组)
+ bundled_items?: any[]; // 捆绑项列表(数组)
@ApiProperty({
example: '2022-12-12 11:11:11',
diff --git a/src/entity/order_item_original.entity.ts b/src/entity/order_item_original.entity.ts
index 26c494a..d7ed055 100644
--- a/src/entity/order_item_original.entity.ts
+++ b/src/entity/order_item_original.entity.ts
@@ -11,9 +11,9 @@ import {
} from 'typeorm';
import { Order } from './order.entity';
-@Entity('order_sale_original')
+@Entity('order_item_original')
@Exclude()
-export class OrderSaleOriginal {
+export class OrderItemOriginal {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
@@ -27,9 +27,9 @@ export class OrderSaleOriginal {
orderId: number; // 订单 ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 站点ID
@ApiProperty()
@Column({ nullable: true })
diff --git a/src/entity/order_refund.entity.ts b/src/entity/order_refund.entity.ts
index a9d5251..4b6a869 100644
--- a/src/entity/order_refund.entity.ts
+++ b/src/entity/order_refund.entity.ts
@@ -22,9 +22,9 @@ export class OrderRefund {
orderId: number; // 订单 ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 站点ID
@ApiProperty()
@Column()
diff --git a/src/entity/order_retund_item.entity.ts b/src/entity/order_refund_item.entity.ts
similarity index 97%
rename from src/entity/order_retund_item.entity.ts
rename to src/entity/order_refund_item.entity.ts
index e148bbf..76cdebc 100644
--- a/src/entity/order_retund_item.entity.ts
+++ b/src/entity/order_refund_item.entity.ts
@@ -22,9 +22,9 @@ export class OrderRefundItem {
refundId: number; // 订单 refund ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 站点ID
@ApiProperty()
@Column()
diff --git a/src/entity/order_sale.entity.ts b/src/entity/order_sale.entity.ts
index e7958ed..eec088f 100644
--- a/src/entity/order_sale.entity.ts
+++ b/src/entity/order_sale.entity.ts
@@ -24,9 +24,9 @@ export class OrderSale {
orderId: number; // 订单 ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string; // 来源站点唯一标识
+ siteId: number; // 来源站点唯一标识
@ApiProperty()
@Column({ nullable: true })
diff --git a/src/entity/order_shipping.entity.ts b/src/entity/order_shipping.entity.ts
index f32e85e..8220992 100644
--- a/src/entity/order_shipping.entity.ts
+++ b/src/entity/order_shipping.entity.ts
@@ -22,9 +22,9 @@ export class OrderShipping {
orderId: number; // 订单 ID
@ApiProperty()
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string;
+ siteId: number; // 站点ID
@ApiProperty()
@Column()
diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts
index e328725..59dbfb8 100644
--- a/src/entity/product.entity.ts
+++ b/src/entity/product.entity.ts
@@ -4,8 +4,17 @@ import {
CreateDateColumn,
UpdateDateColumn,
Entity,
+ ManyToMany,
+ JoinTable,
+ OneToMany,
+ ManyToOne,
+ JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
+import { DictItem } from './dict_item.entity';
+import { ProductStockComponent } from './product_stock_component.entity';
+import { ProductSiteSku } from './product_site_sku.entity';
+import { Category } from './category.entity';
@Entity()
export class Product {
@@ -17,6 +26,14 @@ export class Product {
})
@PrimaryGeneratedColumn()
id: number;
+
+ @ApiProperty({ description: 'sku'})
+ @Column({ unique: true })
+ sku: string;
+ // 类型 主要用来区分混装和单品 单品死
+ @ApiProperty({ description: '类型' })
+ @Column({ length: 16, default: 'single' })
+ type: string;
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
@@ -27,33 +44,55 @@ export class Product {
@Column()
name: string;
- @ApiProperty()
- @Column({ default: ''})
+ @ApiProperty({ description: '产品中文名称' })
+ @Column({ default: '' })
nameCn: string;
- @ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
+ @ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
+ @Column({ nullable: true })
+ shortDescription?: string;
+
+ @ApiProperty({ example: '产品描述', description: '产品描述' })
@Column({ nullable: true })
description?: string;
- @ApiProperty({ example: '1', description: '分类 ID', type: 'number' })
- @Column()
- categoryId: number;
+ // 商品价格
+ @ApiProperty({ description: '价格', example: 99.99 })
+ @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
+ price: number;
- @ApiProperty()
- @Column()
- flavorsId: number;
+ // 促销价格
+ @ApiProperty({ description: '促销价格', example: 99.99 })
+ @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
+ promotionPrice: number;
- @ApiProperty()
- @Column()
- strengthId: number;
- @ApiProperty()
- @Column()
- humidity: string;
- @ApiProperty({ description: 'sku', type: 'string' })
- @Column({ nullable: true })
- sku?: string;
+
+ // 分类关联
+ @ManyToOne(() => Category, category => category.products)
+ @JoinColumn({ name: 'categoryId' })
+ category: Category;
+
+ @ManyToMany(() => DictItem, dictItem => dictItem.products, {
+ cascade: true,
+ })
+ @JoinTable()
+ attributes: DictItem[];
+
+ // 产品的库存组成,一对多关系(使用独立表)
+ @ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
+ @OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
+ components: ProductStockComponent[];
+
+ @ApiProperty({ description: '站点 SKU 列表', type: ProductSiteSku, isArray: true })
+ @OneToMany(() => ProductSiteSku, (siteSku) => siteSku.product, { cascade: true })
+ siteSkus: ProductSiteSku[];
+
+ // 来源
+ @ApiProperty({ description: '来源', example: '1' })
+ @Column({ default: 0 })
+ source: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
diff --git a/src/entity/product_site_sku.entity.ts b/src/entity/product_site_sku.entity.ts
new file mode 100644
index 0000000..c91c172
--- /dev/null
+++ b/src/entity/product_site_sku.entity.ts
@@ -0,0 +1,36 @@
+import {
+ PrimaryGeneratedColumn,
+ Column,
+ CreateDateColumn,
+ UpdateDateColumn,
+ Entity,
+ ManyToOne,
+ JoinColumn,
+} from 'typeorm';
+import { ApiProperty } from '@midwayjs/swagger';
+import { Product } from './product.entity';
+
+@Entity('product_site_sku')
+export class ProductSiteSku {
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @ApiProperty({ description: '站点 SKU' })
+ @Column({ length: 100, comment: '站点 SKU' })
+ siteSku: string;
+
+ @ManyToOne(() => Product, product => product.siteSkus, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({ name: 'productId' })
+ product: Product;
+
+ @Column()
+ productId: number;
+
+ @CreateDateColumn()
+ createdAt: Date;
+
+ @UpdateDateColumn()
+ updatedAt: Date;
+}
diff --git a/src/entity/product_stock_component.entity.ts b/src/entity/product_stock_component.entity.ts
new file mode 100644
index 0000000..4048070
--- /dev/null
+++ b/src/entity/product_stock_component.entity.ts
@@ -0,0 +1,34 @@
+import { ApiProperty } from '@midwayjs/swagger';
+import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import { Product } from './product.entity';
+
+@Entity('product_stock_component')
+export class ProductStockComponent {
+ @ApiProperty({ type: Number })
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @ApiProperty({ type: Number })
+ @Column()
+ productId: number;
+
+ @ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
+ @Column({ type: 'varchar', length: 64 })
+ sku: string;
+
+ @ApiProperty({ type: Number, description: '组成数量' })
+ @Column({ type: 'int', default: 1 })
+ quantity: number;
+
+ // 多对一,组件隶属于一个产品
+ @ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
+ product: Product;
+
+ @ApiProperty({ description: '创建时间' })
+ @CreateDateColumn()
+ createdAt: Date;
+
+ @ApiProperty({ description: '更新时间' })
+ @UpdateDateColumn()
+ updatedAt: Date;
+}
diff --git a/src/entity/purchase_order_item.entity.ts b/src/entity/purchase_order_item.entity.ts
index 5144ccd..4d4b52b 100644
--- a/src/entity/purchase_order_item.entity.ts
+++ b/src/entity/purchase_order_item.entity.ts
@@ -10,11 +10,11 @@ export class PurchaseOrderItem {
@ApiProperty({ type: String })
@Column()
- productSku: string;
+ sku: string;
@ApiProperty({ type: String })
@Column()
- productName: string;
+ name: string;
@ApiProperty({ type: Number })
@Column()
diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts
index d4e3215..7e03f4b 100644
--- a/src/entity/site.entity.ts
+++ b/src/entity/site.entity.ts
@@ -1,28 +1,47 @@
-import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
+import { Area } from './area.entity';
+import { StockPoint } from './stock_point.entity';
@Entity('site')
export class Site {
- @PrimaryGeneratedColumn({ type: 'int' })
+ @PrimaryGeneratedColumn()
id: number;
- @Column({ type: 'varchar', length: 255, nullable: true })
+ @Column({ length: 255, nullable: true })
apiUrl: string;
- @Column({ type: 'varchar', length: 255, nullable: true })
- consumerKey: string;
+ @Column({ name: 'website_url', length: 255, nullable: true })
+ websiteUrl: string;
- @Column({ type: 'varchar', length: 255, nullable: true })
- consumerSecret: string;
+ @Column({ length: 255, nullable: true })
+ consumerKey?: string;
- @Column({ type: 'varchar', length: 255, unique: true })
- siteName: string;
+ @Column({ length: 255, nullable: true })
+ consumerSecret?: string;
- @Column({ type: 'varchar', length: 32, default: 'woocommerce' })
- type: string; // 平台类型:woocommerce | shopyy
+ @Column({ nullable: true })
+ token?: string;
- @Column({ type: 'varchar', length: 64, nullable: true })
+ @Column({ length: 255, unique: true })
+ name: string;
+
+ @Column({ length: 255, nullable: true })
+ description?: string;
+
+ @Column({ length: 32, default: 'woocommerce' })
+ type: string; // 平台类型:woocommerce | shopyy
+
+ @Column({ length: 64, nullable: true })
skuPrefix: string;
- @Column({ type: 'tinyint', default: 0 })
- isDisabled: number;
+ @Column({ default: false })
+ isDisabled: boolean;
+
+ @ManyToMany(() => Area)
+ @JoinTable()
+ areas: Area[];
+
+ @ManyToMany(() => StockPoint, stockPoint => stockPoint.sites)
+ @JoinTable()
+ stockPoints: StockPoint[];
}
\ No newline at end of file
diff --git a/src/entity/stock.entity.ts b/src/entity/stock.entity.ts
index 233cda7..3977830 100644
--- a/src/entity/stock.entity.ts
+++ b/src/entity/stock.entity.ts
@@ -20,7 +20,7 @@ export class Stock {
@ApiProperty({ type: String })
@Column()
- productSku: string;
+ sku: string;
@ApiProperty({ type: Number })
@Column()
diff --git a/src/entity/stock_point.entity.ts b/src/entity/stock_point.entity.ts
index ffb14d6..dda7ea8 100644
--- a/src/entity/stock_point.entity.ts
+++ b/src/entity/stock_point.entity.ts
@@ -8,8 +8,12 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
OneToMany,
+ ManyToMany,
+ JoinTable,
} from 'typeorm';
import { Shipment } from './shipment.entity';
+import { Area } from './area.entity';
+import { Site } from './site.entity';
@Entity('stock_point')
export class StockPoint extends BaseEntity {
@@ -51,7 +55,7 @@ export class StockPoint extends BaseEntity {
@Column({ default: 'uniuni' })
upStreamName: string;
- @Column()
+ @Column({ default: 0 })
upStreamStockPointId: number;
@ApiProperty({
@@ -72,4 +76,11 @@ export class StockPoint extends BaseEntity {
@DeleteDateColumn()
deletedAt: Date; // 软删除时间
+
+ @ManyToMany(() => Area)
+ @JoinTable()
+ areas: Area[];
+
+ @ManyToMany(() => Site, site => site.stockPoints)
+ sites: Site[];
}
diff --git a/src/entity/stock_record.entity.ts b/src/entity/stock_record.entity.ts
index 0d48882..bd6cf81 100644
--- a/src/entity/stock_record.entity.ts
+++ b/src/entity/stock_record.entity.ts
@@ -20,7 +20,7 @@ export class StockRecord {
@ApiProperty({ type: String })
@Column()
- productSku: string;
+ sku: string;
@ApiProperty({ type: StockRecordOperationType })
@Column({ type: 'enum', enum: StockRecordOperationType })
diff --git a/src/entity/strength.entity.ts b/src/entity/strength.entity.ts
deleted file mode 100644
index 9ffc916..0000000
--- a/src/entity/strength.entity.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import {
- PrimaryGeneratedColumn,
- Column,
- CreateDateColumn,
- UpdateDateColumn,
- Entity,
-} from 'typeorm';
-import { ApiProperty } from '@midwayjs/swagger';
-
-@Entity()
-export class Strength {
- @ApiProperty()
- @PrimaryGeneratedColumn()
- id: number;
-
- @ApiProperty()
- @Column()
- name: string;
-
- @ApiProperty({
- description: '唯一识别key',
- type: 'string',
- required: true,
- })
- @Column()
- unique_key: string;
-
- @ApiProperty({
- example: '2022-12-12 11:11:11',
- description: '创建时间',
- required: true,
- })
- @CreateDateColumn()
- createdAt: Date;
-
- @ApiProperty({
- example: '2022-12-12 11:11:11',
- description: '更新时间',
- required: true,
- })
- @UpdateDateColumn()
- updatedAt: Date;
-}
diff --git a/src/entity/subscription.entity.ts b/src/entity/subscription.entity.ts
index f4c76bb..8ec28e2 100644
--- a/src/entity/subscription.entity.ts
+++ b/src/entity/subscription.entity.ts
@@ -12,68 +12,68 @@ import { SubscriptionStatus } from '../enums/base.enum';
@Entity('subscription')
@Exclude()
export class Subscription {
- // 本地主键,自增 ID
+ // 本地主键,自增 ID
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
- // 站点唯一标识,用于区分不同来源站点
+ // 站点唯一标识,用于区分不同来源站点
@ApiProperty({ description: '来源站点唯一标识' })
- @Column()
+ @Column({ nullable: true })
@Expose()
- siteId: string;
+ siteId: number;
- // WooCommerce 订阅的原始 ID(字符串化),用于幂等更新
+ // WooCommerce 订阅的原始 ID(字符串化),用于幂等更新
@ApiProperty({ description: 'WooCommerce 订阅 ID' })
@Column()
@Expose()
externalSubscriptionId: string;
- // 订阅状态(active/cancelled/on-hold 等)
+ // 订阅状态(active/cancelled/on-hold 等)
@ApiProperty({ type: SubscriptionStatus })
@Column({ type: 'enum', enum: SubscriptionStatus })
@Expose()
status: SubscriptionStatus;
- // 货币代码,例如 USD/CAD
+ // 货币代码,例如 USD/CAD
@ApiProperty()
@Column({ default: '' })
@Expose()
currency: string;
- // 总金额,保留两位小数
+ // 总金额,保留两位小数
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total: number;
- // 计费周期(day/week/month/year)
+ // 计费周期(day/week/month/year)
@ApiProperty({ description: '计费周期 e.g. day/week/month/year' })
@Column({ default: '' })
@Expose()
billing_period: string;
- // 计费周期间隔(例如 1/3/12)
+ // 计费周期间隔(例如 1/3/12)
@ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' })
@Column({ type: 'int', default: 0 })
@Expose()
billing_interval: number;
- // 客户 ID(WooCommerce 用户 ID)
+ // 客户 ID(WooCommerce 用户 ID)
@ApiProperty()
@Column({ type: 'int', default: 0 })
@Expose()
customer_id: number;
- // 客户邮箱(从 billing.email 或 customer_email 提取)
+ // 客户邮箱(从 billing.email 或 customer_email 提取)
@ApiProperty()
@Column({ default: '' })
@Expose()
customer_email: string;
- // 父订单/订阅 ID(如有)
- @ApiProperty({ description: '父订单/父订阅ID(如有)' })
+ // 父订单/订阅 ID(如有)
+ @ApiProperty({ description: '父订单/父订阅ID(如有)' })
@Column({ type: 'int', default: 0 })
@Expose()
parent_id: number;
@@ -102,25 +102,25 @@ export class Subscription {
@Expose()
end_date: Date;
- // 商品项(订阅行项目)
+ // 商品项(订阅行项目)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
line_items: any[];
- // 额外元数据(键值对)
+ // 额外元数据(键值对)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: any[];
- // 创建时间(数据库自动生成)
+ // 创建时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
@CreateDateColumn()
@Expose()
createdAt: Date;
- // 更新时间(数据库自动生成)
+ // 更新时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
@UpdateDateColumn()
@Expose()
diff --git a/src/entity/template.entity.ts b/src/entity/template.entity.ts
new file mode 100644
index 0000000..13fee49
--- /dev/null
+++ b/src/entity/template.entity.ts
@@ -0,0 +1,54 @@
+import { ApiProperty } from '@midwayjs/swagger';
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+
+@Entity('template')
+export class Template {
+ @ApiProperty({ type: 'number' })
+ @PrimaryGeneratedColumn()
+ id: number;
+
+ @ApiProperty({ type: 'string' })
+ @Column({ unique: true })
+ name: string;
+
+ @ApiProperty({ type: 'string' })
+ @Column('text')
+ value: string;
+
+ @ApiProperty({ nullable: true ,name:"描述"})
+ @Column('text',{nullable: true,comment: "描述"})
+ description?: string;
+
+ @ApiProperty({ type: 'string', nullable: true, description: '测试数据JSON' })
+ @Column('text', { nullable: true, comment: '测试数据JSON' })
+ testData?: string;
+
+ @ApiProperty({
+ example: true,
+ description: '是否可删除',
+ required: true,
+ })
+ @Column({ default: true })
+ deletable: boolean;
+ @ApiProperty({
+ example: '2022-12-12 11:11:11',
+ description: '创建时间',
+ required: true,
+ })
+ @CreateDateColumn()
+ createdAt: Date;
+
+ @ApiProperty({
+ example: '2022-12-12 11:11:11',
+ description: '更新时间',
+ required: true,
+ })
+ @UpdateDateColumn()
+ updatedAt: Date;
+}
diff --git a/src/entity/transfer_item.entity.ts b/src/entity/transfer_item.entity.ts
index 312f6dd..6c72f54 100644
--- a/src/entity/transfer_item.entity.ts
+++ b/src/entity/transfer_item.entity.ts
@@ -9,11 +9,11 @@ export class TransferItem {
@ApiProperty({ type: String })
@Column()
- productSku: string;
+ sku: string;
@ApiProperty({ type: String })
@Column()
- productName: string;
+ name: string;
@ApiProperty({ type: Number })
@Column()
diff --git a/src/entity/user.entity.ts b/src/entity/user.entity.ts
index 754490e..3b4045f 100644
--- a/src/entity/user.entity.ts
+++ b/src/entity/user.entity.ts
@@ -15,10 +15,14 @@ export class User {
password: string;
// @Column() // 默认角色为管理员
- // roleId: number; // 角色 (如:admin, editor, viewer)
+ // roleId: number; // 角色 (如:admin, editor, viewer)
@Column({ type: 'simple-array', nullable: true })
- permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
+ permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
+
+ // 新增邮箱字段,可选且唯一
+ @Column({ unique: true, nullable: true })
+ email?: string;
@Column({ default: false })
isSuper: boolean; // 超级管理员
@@ -28,4 +32,8 @@ export class User {
@Column({ default: true })
isActive: boolean; // 用户是否启用
+
+ // 备注字段(可选)
+ @Column({ nullable: true })
+ remark?: string;
}
diff --git a/src/entity/variation.entity.ts b/src/entity/variation.entity.ts
index 7550bb5..b3232f5 100644
--- a/src/entity/variation.entity.ts
+++ b/src/entity/variation.entity.ts
@@ -21,13 +21,12 @@ export class Variation {
id: number;
@ApiProperty({
- example: '1',
- description: 'wp网站ID',
- type: 'string',
- required: true,
+ description: '站点 id',
+ example: 1,
+ required: false
})
- @Column()
- siteId: string; // 来源站点唯一标识
+ @Column({ nullable: true })
+ siteId: number;
@ApiProperty({
example: '1',
@@ -102,18 +101,4 @@ export class Variation {
})
@UpdateDateColumn()
updatedAt: Date;
-
- @ApiProperty({
- description: '变体构成成分',
- type: 'array',
- items: {
- type: 'object',
- properties: {
- sku: { type: 'string' },
- quantity: { type: 'number' },
- },
- },
- })
- @Column('json', { nullable: true, comment: '变体构成成分' })
- constitution: { sku: string; quantity: number }[] | null;
}
diff --git a/src/entity/wp_product.entity.ts b/src/entity/wp_product.entity.ts
index 3cabe1c..b0327db 100644
--- a/src/entity/wp_product.entity.ts
+++ b/src/entity/wp_product.entity.ts
@@ -1,3 +1,4 @@
+import { Site } from './site.entity';
import {
PrimaryGeneratedColumn,
Column,
@@ -5,6 +6,8 @@ import {
UpdateDateColumn,
Unique,
Entity,
+ ManyToOne,
+ JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { ProductStatus, ProductStockStatus, ProductType } from '../enums/base.enum';
@@ -22,13 +25,18 @@ export class WpProduct {
id: number;
@ApiProperty({
- example: '1',
+ example: 1,
description: 'wp网站ID',
- type: 'string',
+ type: 'number',
required: true,
})
- @Column()
- siteId: string;
+ @Column({ type: 'int', nullable: true })
+ siteId: number;
+
+ @ApiProperty({ description: '站点信息', type: Site })
+ @ManyToOne(() => Site)
+ @JoinColumn({ name: 'siteId', referencedColumnName: 'id' })
+ site: Site;
@ApiProperty({
example: '1',
@@ -39,7 +47,7 @@ export class WpProduct {
@Column()
externalProductId: string;
- @ApiProperty({ description: 'sku', type: 'string' })
+ @ApiProperty({ description: '商店sku', type: 'string' })
@Column({ nullable: true })
sku?: string;
@@ -53,42 +61,150 @@ export class WpProduct {
name: string;
@ApiProperty({ description: '产品状态', enum: ProductStatus })
- @Column({ type: 'enum', enum: ProductStatus })
+ @Column({ type: 'enum', enum: ProductStatus, comment: '产品状态: draft, pending, private, publish' })
status: ProductStatus;
+ @ApiProperty({ description: '是否为特色产品', type: 'boolean' })
+ @Column({ default: false, comment: '是否为特色产品' })
+ featured: boolean;
+
+ @ApiProperty({ description: '目录可见性', type: 'string' })
+ @Column({ default: 'visible', comment: '目录可见性: visible, catalog, search, hidden' })
+ catalog_visibility: string;
+
+ @ApiProperty({ description: '产品描述', type: 'string' })
+ @Column({ type: 'text', nullable: true, comment: '产品描述' })
+ description: string;
+
+ @ApiProperty({ description: '产品短描述', type: 'string' })
+ @Column({ type: 'text', nullable: true, comment: '产品短描述' })
+ short_description: string;
+
@ApiProperty({ description: '上下架状态', enum: ProductStockStatus })
@Column({
name: 'stock_status',
type: 'enum',
enum: ProductStockStatus,
- default: ProductStockStatus.INSTOCK
+ default: ProductStockStatus.INSTOCK,
+ comment: '库存状态: instock, outofstock, onbackorder',
})
stockStatus: ProductStockStatus;
+ @ApiProperty({ description: '库存数量', type: 'number' })
+ @Column({ type: 'int', nullable: true, comment: '库存数量' })
+ stock_quantity: number;
+
+ @ApiProperty({ description: '允许缺货下单', type: 'string' })
+ @Column({ nullable: true, comment: '允许缺货下单: no, notify, yes' })
+ backorders: string;
+
+ @ApiProperty({ description: '是否单独出售', type: 'boolean' })
+ @Column({ default: false, comment: '是否单独出售' })
+ sold_individually: boolean;
+
@ApiProperty({ description: '常规价格', type: Number })
- @Column('decimal', { precision: 10, scale: 2, nullable: true })
- regular_price: number; // 常规价格
+ @Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '常规价格' })
+ regular_price: number;
@ApiProperty({ description: '销售价格', type: Number })
- @Column('decimal', { precision: 10, scale: 2, nullable: true })
- sale_price: number; // 销售价格
+ @Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '销售价格' })
+ sale_price: number;
+
+ @ApiProperty({ description: '促销开始日期', type: 'datetime' })
+ @Column({ type: 'datetime', nullable: true, comment: '促销开始日期' })
+ date_on_sale_from: Date| null;
+
+ @ApiProperty({ description: '促销结束日期', type: 'datetime' })
+ @Column({ type: 'datetime', nullable: true, comment: '促销结束日期' })
+ date_on_sale_to: Date|null;
@ApiProperty({ description: '是否促销中', type: Boolean })
- @Column({ nullable: true, type: Boolean })
- on_sale: boolean; // 是否促销中
+ @Column({ nullable: true, type: 'boolean', comment: '是否促销中' })
+ on_sale: boolean;
- @ApiProperty({ description: '是否删除', type: Boolean })
- @Column({ nullable: true, type: Boolean , default: false })
- on_delete: boolean; // 是否删除
+ @ApiProperty({ description: '税务状态', type: 'string' })
+ @Column({ default: 'taxable', comment: '税务状态: taxable, shipping, none' })
+ tax_status: string;
+ @ApiProperty({ description: '税类', type: 'string' })
+ @Column({ nullable: true, comment: '税类' })
+ tax_class: string;
- @ApiProperty({
- description: '产品类型',
- enum: ProductType,
- })
- @Column({ type: 'enum', enum: ProductType })
+ @ApiProperty({ description: '重量(g)', type: 'number' })
+ @Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '重量(g)' })
+ weight: number;
+
+ @ApiProperty({ description: '尺寸(长宽高)', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '尺寸' })
+ dimensions: { length: string; width: string; height: string };
+
+ @ApiProperty({ description: '允许评论', type: 'boolean' })
+ @Column({ default: true, comment: '允许客户评论' })
+ reviews_allowed: boolean;
+
+ @ApiProperty({ description: '购买备注', type: 'string' })
+ @Column({ nullable: true, comment: '购买备注' })
+ purchase_note: string;
+
+ @ApiProperty({ description: '菜单排序', type: 'number' })
+ @Column({ default: 0, comment: '菜单排序' })
+ menu_order: number;
+
+ @ApiProperty({ description: '产品类型', enum: ProductType })
+ @Column({ type: 'enum', enum: ProductType, comment: '产品类型: simple, grouped, external, variable' })
type: ProductType;
+ @ApiProperty({ description: '父产品ID', type: 'number' })
+ @Column({ default: 0, comment: '父产品ID' })
+ parent_id: number;
+
+ @ApiProperty({ description: '外部产品URL', type: 'string' })
+ @Column({ type: 'text', nullable: true, comment: '外部产品URL' })
+ external_url: string;
+
+ @ApiProperty({ description: '外部产品按钮文本', type: 'string' })
+ @Column({ nullable: true, comment: '外部产品按钮文本' })
+ button_text: string;
+
+ @ApiProperty({ description: '分组产品', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '分组产品' })
+ grouped_products: number[];
+
+ @ApiProperty({ description: '追加销售', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '追加销售' })
+ upsell_ids: number[];
+
+ @ApiProperty({ description: '交叉销售', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '交叉销售' })
+ cross_sell_ids: number[];
+
+ @ApiProperty({ description: '分类', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '分类' })
+ categories: { id: number; name: string; slug: string }[];
+
+ @ApiProperty({ description: '标签', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '标签' })
+ tags: { id: number; name: string; slug: string }[];
+
+ @ApiProperty({ description: '图片', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '图片' })
+ images: { id: number; src: string; name: string; alt: string }[];
+
+ @ApiProperty({ description: '产品属性', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '产品属性' })
+ attributes: { id: number; name: string; position: number; visible: boolean; variation: boolean; options: string[] }[];
+
+ @ApiProperty({ description: '默认属性', type: 'json' })
+ @Column({ type: 'json', nullable: true, comment: '默认属性' })
+ default_attributes: { id: number; name: string; option: string }[];
+
+ @ApiProperty({ description: 'GTIN', type: 'string' })
+ @Column({ nullable: true, comment: 'GTIN, UPC, EAN, or ISBN' })
+ gtin: string;
+
+ @ApiProperty({ description: '是否删除', type: 'boolean' })
+ @Column({ nullable: true, type: 'boolean', default: false, comment: '是否删除' })
+ on_delete: boolean;
@Column({ type: 'json', nullable: true })
metadata: Record; // 产品的其他扩展字段
@@ -108,18 +224,4 @@ export class WpProduct {
})
@UpdateDateColumn()
updatedAt: Date;
-
- @ApiProperty({
- description: '产品构成成分',
- type: 'array',
- items: {
- type: 'object',
- properties: {
- sku: { type: 'string' },
- quantity: { type: 'number' },
- },
- },
- })
- @Column('json', { nullable: true, comment: '产品构成成分' })
- constitution: { sku: string; quantity: number }[] | null;
}
diff --git a/src/enums/base.enum.ts b/src/enums/base.enum.ts
index 085a582..95903d3 100644
--- a/src/enums/base.enum.ts
+++ b/src/enums/base.enum.ts
@@ -42,7 +42,7 @@ export enum OrderStatus {
REFUNDED = 'refunded', // 已退款
FAILED = 'failed', // 失败订单
DRAFT = 'draft', // 草稿
- AUTO_DRAFT = 'auto-draft', // 自动草稿
+
// TRASH = 'trash',
// refund 也就是退款相关的状态
RETURN_REQUESTED = 'return-requested', // 已申请退款
diff --git a/src/interface.ts b/src/interface.ts
index f7b94f9..e000403 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -10,7 +10,7 @@ export interface WpSite {
wpApiUrl: string;
consumerKey: string;
consumerSecret: string;
- siteName: string;
+ name: string;
email: string;
emailPswd: string;
}
diff --git a/src/interface/platform.interface.ts b/src/interface/platform.interface.ts
new file mode 100644
index 0000000..bc5e4d8
--- /dev/null
+++ b/src/interface/platform.interface.ts
@@ -0,0 +1,287 @@
+// src/interface/platform.interface.ts
+
+/**
+ * 电商平台抽象接口
+ * 定义所有平台必须实现的通用方法
+ */
+export interface IPlatformService {
+ /**
+ * 获取产品列表
+ * @param site 站点配置信息
+ * @returns 产品列表数据
+ */
+ getProducts(site: any): Promise;
+
+ /**
+ * 获取单个产品
+ * @param site 站点配置信息
+ * @param id 产品ID
+ * @returns 产品数据
+ */
+ getProduct(site: any, id: number): Promise;
+
+ /**
+ * 获取产品变体列表
+ * @param site 站点配置信息
+ * @param productId 产品ID
+ * @returns 变体列表数据
+ */
+ getVariations(site: any, productId: number): Promise;
+
+ /**
+ * 获取产品变体详情
+ * @param site 站点配置信息
+ * @param productId 产品ID
+ * @param variationId 变体ID
+ * @returns 变体详情数据
+ */
+ getVariation(site: any, productId: number, variationId: number): Promise;
+
+ /**
+ * 获取订单列表
+ * @param siteId 站点ID
+ * @returns 订单列表数据
+ */
+ getOrders(siteId: number, params: Record): Promise;
+
+ /**
+ * 获取订单详情
+ * @param siteId 站点ID
+ * @param orderId 订单ID
+ * @returns 订单详情数据
+ */
+ getOrder(siteId: number, orderId: string): Promise;
+
+ /**
+ * 获取订阅列表(如果平台支持)
+ * @param siteId 站点ID
+ * @returns 订阅列表数据
+ */
+ getSubscriptions?(siteId: number): Promise;
+
+ /**
+ * 获取客户列表
+ * @param site 站点配置信息
+ * @returns 客户列表数据
+ */
+ getCustomers(site: any): Promise;
+
+ /**
+ * 获取单个客户
+ * @param site 站点配置信息
+ * @param id 客户ID
+ * @returns 客户数据
+ */
+ getCustomer(site: any, id: number): Promise;
+
+ /**
+ * 创建产品
+ * @param site 站点配置信息
+ * @param data 产品数据
+ * @returns 创建结果
+ */
+ createProduct(site: any, data: any): Promise;
+
+ /**
+ * 更新产品
+ * @param site 站点配置信息
+ * @param productId 产品ID
+ * @param data 更新数据
+ * @returns 更新结果
+ */
+ updateProduct(site: any, productId: string, data: any): Promise;
+
+ /**
+ * 更新产品状态
+ * @param site 站点配置信息
+ * @param productId 产品ID
+ * @param status 产品状态
+ * @param stockStatus 库存状态
+ * @returns 更新结果
+ */
+ updateProductStatus(site: any, productId: string, status: string, stockStatus: string): Promise;
+
+ /**
+ * 更新产品变体
+ * @param site 站点配置信息
+ * @param productId 产品ID
+ * @param variationId 变体ID
+ * @param data 更新数据
+ * @returns 更新结果
+ */
+ updateVariation(site: any, productId: string, variationId: string, data: any): Promise;
+
+ /**
+ * 更新订单
+ * @param site 站点配置信息
+ * @param orderId 订单ID
+ * @param data 更新数据
+ * @returns 更新结果
+ */
+ updateOrder(site: any, orderId: string, data: Record): Promise;
+
+ /**
+ * 创建物流信息
+ * @param site 站点配置信息
+ * @param orderId 订单ID
+ * @param data 物流数据
+ * @returns 创建结果
+ */
+ createShipment(site: any, orderId: string, data: any): Promise;
+
+ /**
+ * 删除物流信息
+ * @param site 站点配置信息
+ * @param orderId 订单ID
+ * @param trackingId 物流跟踪ID
+ * @returns 删除结果
+ */
+ deleteShipment(site: any, orderId: string, trackingId: string): Promise;
+
+ /**
+ * 批量处理产品
+ * @param site 站点配置信息
+ * @param data 批量操作数据
+ * @returns 处理结果
+ */
+ batchProcessProducts(site: any, data: { create?: any[]; update?: any[]; delete?: any[] }): Promise;
+
+ /**
+ * 获取 api 客户端
+ * @param site 站点配置信息
+ * @returns api 客户端
+ */
+ getApiClient(site: any): any;
+
+ /**
+ * 获取客户列表
+ * @param site 站点配置信息
+ * @returns 客户列表数据
+ */
+ getCustomers(site: any): Promise;
+
+ /**
+ * 获取单个客户
+ * @param site 站点配置信息
+ * @param id 客户ID
+ * @returns 客户数据
+ */
+ getCustomer(site: any, id: number): Promise;
+
+ /**
+ * 获取评论列表
+ * @param site 站点配置信息
+ * @returns 评论列表数据
+ */
+ getReviews(site: any): Promise;
+
+ /**
+ * 创建评论
+ * @param site 站点配置信息
+ * @param data 评论数据
+ * @returns 创建结果
+ */
+ createReview(site: any, data: any): Promise;
+
+ /**
+ * 更新评论
+ * @param site 站点配置信息
+ * @param reviewId 评论ID
+ * @param data 更新数据
+ * @returns 更新结果
+ */
+ updateReview(site: any, reviewId: number, data: any): Promise;
+
+ /**
+ * 删除评论
+ * @param site 站点配置信息
+ * @param reviewId 评论ID
+ * @returns 删除结果
+ */
+ deleteReview(site: any, reviewId: number): Promise;
+
+ /**
+ * 获取分页资源
+ * @param site 站点配置信息
+ * @param resource 资源类型
+ * @param params 查询参数
+ * @param namespace API命名空间
+ * @returns 分页数据
+ */
+ fetchResourcePaged(site: any, resource: string, params: Record, namespace?: any): Promise<{ items: T[]; total: number; totalPages: number; page: number; per_page: number }>;
+
+ /**
+ * 获取分页媒体
+ * @param site 站点配置信息
+ * @param params 查询参数
+ * @returns 分页媒体数据
+ */
+ fetchMediaPaged(site: any, params: Record): Promise<{ items: any[]; total: number; totalPages: number; page: number; per_page: number }>;
+
+ /**
+ * 删除媒体
+ * @param siteId 站点ID
+ * @param mediaId 媒体ID
+ * @param force 是否强制删除
+ * @returns 删除结果
+ */
+ deleteMedia(siteId: number, mediaId: number, force?: boolean): Promise;
+
+ /**
+ * 更新媒体
+ * @param siteId 站点ID
+ * @param mediaId 媒体ID
+ * @param data 更新数据
+ * @returns 更新结果
+ */
+ updateMedia(siteId: number, mediaId: number, data: any): Promise;
+
+ /**
+ * 转换媒体为WebP格式
+ * @param siteId 站点ID
+ * @param mediaIds 媒体ID列表
+ * @returns 转换结果
+ */
+ convertMediaToWebp(siteId: number, mediaIds: Array): Promise<{ converted: any[]; failed: Array<{ id: number | string; error: string }> }>;
+
+ /**
+ * 获取webhook列表
+ * @param site 站点配置信息
+ * @param params 查询参数
+ * @returns 分页webhook列表
+ */
+ getWebhooks(site: any, params: any): Promise;
+
+ /**
+ * 获取单个webhook
+ * @param site 站点配置信息
+ * @param webhookId webhook ID
+ * @returns webhook详情
+ */
+ getWebhook(site: any, webhookId: string | number): Promise;
+
+ /**
+ * 创建webhook
+ * @param site 站点配置信息
+ * @param data webhook数据
+ * @returns 创建结果
+ */
+ createWebhook(site: any, data: any): Promise;
+
+ /**
+ * 更新webhook
+ * @param site 站点配置信息
+ * @param webhookId webhook ID
+ * @param data 更新数据
+ * @returns 更新结果
+ */
+ updateWebhook(site: any, webhookId: string | number, data: any): Promise;
+
+ /**
+ * 删除webhook
+ * @param site 站点配置信息
+ * @param webhookId webhook ID
+ * @returns 删除结果
+ */
+ deleteWebhook(site: any, webhookId: string | number): Promise;
+}
diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts
new file mode 100644
index 0000000..8872e3f
--- /dev/null
+++ b/src/interface/site-adapter.interface.ts
@@ -0,0 +1,170 @@
+import {
+ CreateReviewDTO,
+ UpdateReviewDTO,
+ UnifiedMediaDTO,
+ UnifiedOrderDTO,
+ UnifiedPaginationDTO,
+ UnifiedProductDTO,
+ UnifiedReviewDTO,
+ UnifiedSearchParamsDTO,
+ UnifiedSubscriptionDTO,
+ UnifiedCustomerDTO,
+ UnifiedWebhookDTO,
+ UnifiedWebhookPaginationDTO,
+ CreateWebhookDTO,
+ UpdateWebhookDTO,
+} from '../dto/site-api.dto';
+
+export interface ISiteAdapter {
+ /**
+ * 获取产品列表
+ */
+ getProducts(params: UnifiedSearchParamsDTO): Promise>;
+
+ /**
+ * 获取单个产品
+ */
+ getProduct(id: string | number): Promise;
+
+ /**
+ * 获取订单列表
+ */
+ getOrders(params: UnifiedSearchParamsDTO): Promise>;
+
+ /**
+ * 获取单个订单
+ */
+ getOrder(id: string | number): Promise;
+
+ /**
+ * 获取订阅列表
+ */
+ getSubscriptions(params: UnifiedSearchParamsDTO): Promise>;
+
+ /**
+ * 获取媒体列表
+ */
+ getMedia(params: UnifiedSearchParamsDTO): Promise>;
+
+ /**
+ * 创建媒体
+ */
+ createMedia(file: any): Promise;
+
+ /**
+ * 获取评论列表
+ */
+ getReviews(params: UnifiedSearchParamsDTO): Promise>;
+
+ /**
+ * 创建评论
+ */
+ createReview(data: CreateReviewDTO): Promise;
+
+ /**
+ * 更新评论
+ */
+ updateReview(id: number, data: UpdateReviewDTO): Promise;
+
+ /**
+ * 删除评论
+ */
+ deleteReview(id: number): Promise;
+
+ /**
+ * 创建产品
+ */
+ createProduct(data: Partial): Promise;
+
+ /**
+ * 更新产品
+ */
+ updateProduct(id: string | number, data: Partial): Promise;
+
+ /**
+ * 更新产品变体
+ */
+ updateVariation(productId: string | number, variationId: string | number, data: any): Promise;
+
+ /**
+ * 获取订单备注
+ */
+ getOrderNotes(orderId: string | number): Promise;
+
+ /**
+ * 创建订单备注
+ */
+ createOrderNote(orderId: string | number, data: any): Promise;
+
+ /**
+ * 删除产品
+ */
+ deleteProduct(id: string | number): Promise;
+
+ batchProcessProducts?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise;
+
+ createOrder(data: Partial): Promise;
+ updateOrder(id: string | number, data: Partial): Promise;
+ deleteOrder(id: string | number): Promise;
+
+ batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise;
+
+ getCustomers(params: UnifiedSearchParamsDTO): Promise>;
+ getCustomer(id: string | number): Promise;
+ createCustomer(data: Partial): Promise;
+ updateCustomer(id: string | number, data: Partial): Promise;
+ deleteCustomer(id: string | number): Promise;
+
+ batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise;
+
+ /**
+ * 获取webhooks列表
+ */
+ getWebhooks(params: UnifiedSearchParamsDTO): Promise;
+
+ /**
+ * 获取单个webhook
+ */
+ getWebhook(id: string | number): Promise;
+
+ /**
+ * 创建webhook
+ */
+ createWebhook(data: CreateWebhookDTO): Promise;
+
+ /**
+ * 更新webhook
+ */
+ updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise;
+
+ /**
+ * 删除webhook
+ */
+ deleteWebhook(id: string | number): Promise;
+
+ /**
+ * 获取站点链接列表
+ */
+ getLinks(): Promise>;
+
+ /**
+ * 订单发货
+ */
+ shipOrder(orderId: string | number, data: {
+ tracking_number?: string;
+ shipping_provider?: string;
+ shipping_method?: string;
+ items?: Array<{
+ order_item_id: number;
+ quantity: number;
+ }>;
+ }): Promise;
+
+ /**
+ * 取消订单发货
+ */
+ cancelShipOrder(orderId: string | number, data: {
+ reason?: string;
+ shipment_id?: string;
+ }): Promise;
+}
diff --git a/src/job/sync_products.job.ts b/src/job/sync_products.job.ts
index fb9b229..d4f4e3e 100644
--- a/src/job/sync_products.job.ts
+++ b/src/job/sync_products.job.ts
@@ -1,15 +1 @@
-import { FORMAT, ILogger, Logger } from '@midwayjs/core';
-import { IJob, Job } from '@midwayjs/cron';
-
-@Job({
- cronTime: FORMAT.CRONTAB.EVERY_DAY,
- runOnInit: true,
-})
-export class SyncProductJob implements IJob {
- @Logger()
- logger: ILogger;
-
- onTick() {
- }
- onComplete?(result: any) {}
-}
+export {}
diff --git a/src/db/seed/index.ts b/src/main.ts
similarity index 100%
rename from src/db/seed/index.ts
rename to src/main.ts
diff --git a/src/middleware/query-normalize.middleware.ts b/src/middleware/query-normalize.middleware.ts
new file mode 100644
index 0000000..e334690
--- /dev/null
+++ b/src/middleware/query-normalize.middleware.ts
@@ -0,0 +1,140 @@
+import { Middleware, IMiddleware } from '@midwayjs/core';
+import { NextFunction, Context } from '@midwayjs/koa';
+import * as qs from 'qs';
+
+@Middleware()
+ export class QueryNormalizeMiddleware implements IMiddleware {
+ // 数值与布尔转换函数,用于将字符串转换为合适的类型
+ private toPrimitive(value: any): any {
+ const s = String(value);
+ if (s === 'true') return true;
+ if (s === 'false') return false;
+ const n = Number(s);
+ return Number.isFinite(n) && s !== '' ? n : value;
+ }
+
+ // 深度遍历对象并对字符串进行trim
+ private trimDeep(input: any): any {
+ if (input === null || input === undefined) return input;
+ if (typeof input === 'string') return input.trim();
+ if (Array.isArray(input)) return input.map(v => this.trimDeep(v));
+ if (typeof input === 'object') {
+ const out: Record = {};
+ for (const key of Object.keys(input)) {
+ out[key] = this.trimDeep((input as any)[key]);
+ }
+ return out;
+ }
+ return input;
+ }
+
+ // 将路径数组对应的值赋到对象中,支持构建嵌套结构与数组
+ private assignByPath(target: Record, path: string[], value: any): void {
+ let cur: any = target;
+ for (let i = 0; i < path.length; i++) {
+ const key = path[i];
+ const isLast = i === path.length - 1;
+ if (isLast) {
+ if (key === '') {
+ if (!Array.isArray(cur)) return;
+ cur.push(value);
+ } else {
+ if (cur[key] === undefined) cur[key] = value;
+ else if (Array.isArray(cur[key])) cur[key].push(value);
+ else cur[key] = [cur[key], value];
+ }
+ } else {
+ if (!cur[key] || typeof cur[key] !== 'object') cur[key] = {};
+ cur = cur[key];
+ }
+ }
+ }
+
+ // 解析可能为 JSON 字符串或鍵值串的输入为对象
+ private parseLooseObject(input: any): Record {
+ if (!input) return {};
+ if (typeof input === 'object') return input as Record;
+ const str = String(input).trim();
+ try {
+ if (str.startsWith('{') || str.startsWith('[')) {
+ const json = JSON.parse(str);
+ if (json && typeof json === 'object') return json as Record;
+ }
+ } catch {}
+ const obj: Record = {};
+ const pairs = str.split(/[&;,]/).map(s => s.trim()).filter(Boolean);
+ for (const pair of pairs) {
+ const idxEq = pair.indexOf('=');
+ const idxColon = pair.indexOf(':');
+ const idx = idxEq >= 0 ? idxEq : idxColon;
+ if (idx < 0) continue;
+ const key = decodeURIComponent(pair.slice(0, idx)).trim();
+ const valueRaw = decodeURIComponent(pair.slice(idx + 1)).trim();
+ obj[key] = this.toPrimitive(valueRaw);
+ }
+ return obj;
+ }
+
+ resolve() {
+ return async (ctx: Context, next: NextFunction) => {
+ const raw = String((ctx.request as any).querystring || '');
+ const parsed = qs.parse(raw, { allowDots: true, depth: 10, ignoreQueryPrefix: false, comma: true });
+ const query = { ...(ctx.request.query || {}), ...(parsed as any) } as Record;
+ const trimmedTop: Record = {};
+ for (const k of Object.keys(query)) {
+ const v = (query as any)[k];
+ trimmedTop[k] = typeof v === 'string' ? String(v).trim() : v;
+ }
+ Object.assign(query, trimmedTop);
+
+ // 解析 where 对象,支持 JSON 字符串与括号或点号语法
+ const hasWhereInput = (query as any).where !== undefined;
+ let whereObj: Record = this.parseLooseObject((query as any).where);
+ for (const k of Object.keys(query)) {
+ if (k === 'where') continue;
+ if (k.startsWith('where[') || k.startsWith('where.')) {
+ const pathStr = k.replace(/^where\.?/, '').replace(/\]/g, '').replace(/\[/g, '.');
+ const path = pathStr.split('.');
+ const val = this.toPrimitive((query as any)[k]);
+ this.assignByPath(whereObj, path, val);
+ }
+ }
+ const hasWhereBracketKeys = Object.keys(query).some(k => k.startsWith('where[') || k.startsWith('where.'));
+ if (hasWhereInput || hasWhereBracketKeys) (query as any).where = this.trimDeep(whereObj);
+
+ // 解析 order 对象,支持 JSON 字符串与括号或点号语法
+ const hasOrderInput = (query as any).order !== undefined;
+ let orderObj: Record