chore: 统一代码中的中文标点符号为英文格式

feat(site): 为站点DTO添加token字段

feat(upload): 添加文件上传配置支持CSV导入

refactor(product): 移除product实体中的stock字段并优化DTO

style: 修复代码中的中文标点符号和注释格式

docs: 更新迁移指南和API文档中的标点符号格式

test: 添加标点符号替换脚本用于规范化代码格式
This commit is contained in:
tikkhun 2025-12-02 22:27:03 +08:00
parent b8aee530e8
commit 998e1e31c7
53 changed files with 910 additions and 467 deletions

3
.gitignore vendored
View File

@ -14,4 +14,5 @@ run/
yarn.lock yarn.lock
**/config.prod.ts **/config.prod.ts
**/config.local.ts **/config.local.ts
container container
ai/products-20251202 (1).csv

View File

@ -2,7 +2,7 @@
## 概述 ## 概述
本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能。 本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能.
## API 接口列表 ## API 接口列表
@ -24,8 +24,8 @@
**参数说明** **参数说明**
- `name`: 区域名称 (必填) - `name`: 区域名称 (必填)
- `latitude`: 纬度 (-90 到 90 之间可选) - `latitude`: 纬度 (-90 到 90 之间,可选)
- `longitude`: 经度 (-180 到 180 之间可选) - `longitude`: 经度 (-180 到 180 之间,可选)
**响应示例** **响应示例**
```json ```json
@ -61,8 +61,8 @@
**参数说明** **参数说明**
- `name`: 区域名称 (可选) - `name`: 区域名称 (可选)
- `latitude`: 纬度 (-90 到 90 之间可选) - `latitude`: 纬度 (-90 到 90 之间,可选)
- `longitude`: 经度 (-180 到 180 之间可选) - `longitude`: 经度 (-180 到 180 之间,可选)
**响应示例** **响应示例**
```json ```json
@ -96,7 +96,7 @@
} }
``` ```
### 4. 获取区域列表(分页) ### 4. 获取区域列表(分页)
**请求信息** **请求信息**
- URL: `/api/area` - URL: `/api/area`
@ -105,7 +105,7 @@
- Query Parameters: - Query Parameters:
- `currentPage`: 当前页码 (默认 1) - `currentPage`: 当前页码 (默认 1)
- `pageSize`: 每页数量 (默认 10) - `pageSize`: 每页数量 (默认 10)
- `name`: 区域名称(可选,用于搜索) - `name`: 区域名称(可选,用于搜索)
**响应示例** **响应示例**
```json ```json
@ -186,11 +186,11 @@
## 世界地图实现建议 ## 世界地图实现建议
对于前端实现世界地图并显示区域坐标,推荐以下方案: 对于前端实现世界地图并显示区域坐标,推荐以下方案:
### 1. 使用开源地图库 ### 1. 使用开源地图库
- **Leaflet.js**: 轻量级开源地图库易于集成 - **Leaflet.js**: 轻量级开源地图库,易于集成
- **Mapbox**: 提供丰富的地图样式和交互功能 - **Mapbox**: 提供丰富的地图样式和交互功能
- **Google Maps API**: 功能强大但需要API密钥 - **Google Maps API**: 功能强大但需要API密钥
@ -222,15 +222,15 @@
4. **添加交互功能**: 4. **添加交互功能**:
- 点击标记显示区域详情 - 点击标记显示区域详情
- 搜索和筛选功能 - 搜索和筛选功能
- 编辑坐标功能调用更新API - 编辑坐标功能(调用更新API)
### 3. 坐标输入建议 ### 3. 坐标输入建议
在区域管理界面,可以添加以下功能来辅助坐标输入: 在区域管理界面,可以添加以下功能来辅助坐标输入:
1. 提供搜索框根据地点名称自动获取坐标 1. 提供搜索框,根据地点名称自动获取坐标
2. 集成小型地图允许用户点击选择位置 2. 集成小型地图,允许用户点击选择位置
3. 添加验证确保输入的坐标在有效范围内 3. 添加验证,确保输入的坐标在有效范围内
## 数据模型说明 ## 数据模型说明
@ -238,16 +238,16 @@
| 字段名 | 类型 | 描述 | 是否必填 | | 字段名 | 类型 | 描述 | 是否必填 |
|--------|------|------|----------| |--------|------|------|----------|
| id | number | 区域ID | 否(自动生成) | | id | number | 区域ID | 否(自动生成) |
| name | string | 区域名称 | 是 | | name | string | 区域名称 | 是 |
| latitude | number | 纬度(范围:-90 到 90 | 否 | | latitude | number | 纬度(范围:-90 到 90) | 否 |
| longitude | number | 经度(范围:-180 到 180 | 否 | | longitude | number | 经度(范围:-180 到 180) | 否 |
| createdAt | Date | 创建时间 | 否(自动生成) | | createdAt | Date | 创建时间 | 否(自动生成) |
| updatedAt | Date | 更新时间 | 否(自动生成) | | updatedAt | Date | 更新时间 | 否(自动生成) |
## 错误处理 ## 错误处理
API可能返回的错误信息 API可能返回的错误信息:
- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时 - `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
- `区域不存在`: 当尝试更新或删除不存在的区域时 - `区域不存在`: 当尝试更新或删除不存在的区域时

View File

@ -1,12 +1,12 @@
# 数据库迁移指南 # 数据库迁移指南
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude``longitude` 字段添加到数据库表中。 为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude``longitude` 字段添加到数据库表中.
## 执行迁移步骤 ## 执行迁移步骤
### 1. 生成迁移文件 ### 1. 生成迁移文件
运行以下命令生成迁移文件 运行以下命令生成迁移文件:
```bash ```bash
npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
@ -14,19 +14,19 @@ npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
### 2. 检查迁移文件 ### 2. 检查迁移文件
生成的迁移文件会自动包含添加 `latitude``longitude` 字段的SQL语句。您可以检查文件内容确保迁移逻辑正确。 生成的迁移文件会自动包含添加 `latitude``longitude` 字段的SQL语句.您可以检查文件内容确保迁移逻辑正确.
### 3. 执行迁移 ### 3. 执行迁移
运行以下命令执行迁移,将更改应用到数据库: 运行以下命令执行迁移,将更改应用到数据库:
```bash ```bash
npm run migration:run npm run migration:run
``` ```
## 手动迁移SQL(可选) ## 手动迁移SQL(可选)
如果需要手动执行SQL,可以使用以下语句: 如果需要手动执行SQL,可以使用以下语句:
```sql ```sql
ALTER TABLE `area` ALTER TABLE `area`
@ -34,9 +34,9 @@ ADD COLUMN `latitude` DECIMAL(10,6) NULL AFTER `name`,
ADD COLUMN `longitude` DECIMAL(10,6) NULL AFTER `latitude`; ADD COLUMN `longitude` DECIMAL(10,6) NULL AFTER `latitude`;
``` ```
## 回滚迁移(如需) ## 回滚迁移(如需)
如果遇到问题,可以使用以下命令回滚迁移: 如果遇到问题,可以使用以下命令回滚迁移:
```bash ```bash
npm run typeorm -- migration:revert -d src/db/datasource.ts npm run typeorm -- migration:revert -d src/db/datasource.ts
@ -45,5 +45,5 @@ npm run typeorm -- migration:revert -d src/db/datasource.ts
## 注意事项 ## 注意事项
- 确保在执行迁移前备份数据库 - 确保在执行迁移前备份数据库
- 迁移不会影响现有数据新增字段默认为 NULL - 迁移不会影响现有数据,新增字段默认为 NULL
- 迁移后可以通过API开始为区域添加坐标信息 - 迁移后,可以通过API开始为区域添加坐标信息

164
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@midwayjs/logger": "^3.1.0", "@midwayjs/logger": "^3.1.0",
"@midwayjs/swagger": "^3.20.2", "@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0", "@midwayjs/typeorm": "^3.20.0",
"@midwayjs/upload": "^3.20.16",
"@midwayjs/validate": "^3.20.2", "@midwayjs/validate": "^3.20.2",
"@woocommerce/woocommerce-rest-api": "^1.0.2", "@woocommerce/woocommerce-rest-api": "^1.0.2",
"axios": "^1.13.2", "axios": "^1.13.2",
@ -813,6 +814,19 @@
"node": ">=12" "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": { "node_modules/@midwayjs/validate": {
"version": "3.20.13", "version": "3.20.13",
"resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.13.tgz", "resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.13.tgz",
@ -928,6 +942,12 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
"license": "MIT" "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": { "node_modules/@types/accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz",
@ -1153,6 +1173,18 @@
"node": ">=8.0.0" "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@ -2246,6 +2278,24 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT" "license": "MIT"
}, },
"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": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
@ -2278,6 +2328,23 @@
"reusify": "^1.0.4" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
@ -3607,6 +3674,19 @@
"node": ">=8" "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": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
@ -3641,6 +3721,15 @@
"node": ">= 0.4" "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": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -3777,6 +3866,47 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "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": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@ -4302,6 +4432,23 @@
"node": ">=8" "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": { "node_modules/superagent": {
"version": "8.1.2", "version": "8.1.2",
"resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz", "resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
@ -4402,6 +4549,23 @@
"node": ">=0.6" "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": { "node_modules/tsc-alias": {
"version": "1.8.16", "version": "1.8.16",
"resolved": "https://registry.npmmirror.com/tsc-alias/-/tsc-alias-1.8.16.tgz", "resolved": "https://registry.npmmirror.com/tsc-alias/-/tsc-alias-1.8.16.tgz",

View File

@ -15,6 +15,7 @@
"@midwayjs/logger": "^3.1.0", "@midwayjs/logger": "^3.1.0",
"@midwayjs/swagger": "^3.20.2", "@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0", "@midwayjs/typeorm": "^3.20.0",
"@midwayjs/upload": "^3.20.16",
"@midwayjs/validate": "^3.20.2", "@midwayjs/validate": "^3.20.2",
"@woocommerce/woocommerce-rest-api": "^1.0.2", "@woocommerce/woocommerce-rest-api": "^1.0.2",
"axios": "^1.13.2", "axios": "^1.13.2",

83
replace_punctuation.js Normal file
View File

@ -0,0 +1,83 @@
const fs = require('fs');
const path = require('path');
const map = {
',': ',',
'.': '.',
':': ':',
'?': '?',
'!': '!',
'"': '"',
'"': '"',
''': "'",
''': "'",
'(': '(',
')': ')',
'[': '[',
']': ']',
',': ',',
';': ';'
};
function getAllFiles(dirPath, arrayOfFiles) {
const files = fs.readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(function(file) {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles);
} else {
if (extensions.some(ext => file.endsWith(ext))) {
arrayOfFiles.push(path.join(dirPath, "/", file));
}
}
});
return arrayOfFiles;
}
const srcDirAPI = path.join(__dirname);
const srcDirWEB = path.join(__dirname, '../WEB');
const targetDirs = [srcDirAPI, srcDirWEB];
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.vue', '.html', '.css', '.scss', '.less', '.json', '.md'];
let count = 0;
targetDirs.forEach(dir => {
if (fs.existsSync(dir)) {
const files = getAllFiles(dir);
files.forEach(file => {
// Skip node_modules, .git, dist, build, .idea, .vscode
if (file.includes('/node_modules/') ||
file.includes('/.git/') ||
file.includes('/dist/') ||
file.includes('/build/') ||
file.includes('/.idea/') ||
file.includes('/.vscode/') ||
file.includes('/coverage/')) {
return;
}
let content = fs.readFileSync(file, 'utf8');
let originalContent = content;
for (const [cn, en] of Object.entries(map)) {
const regex = new RegExp(cn, 'g');
content = content.replace(regex, en);
}
if (content !== originalContent) {
fs.writeFileSync(file, content, 'utf8');
console.log(`Updated: ${file}`);
count++;
}
});
} else {
console.warn(`Directory not found: ${dir}`);
}
});
console.log(`Total files updated: ${count}`);

View File

@ -107,7 +107,7 @@ export default {
// origin: '*', // 允许所有来源跨域请求 // origin: '*', // 允许所有来源跨域请求
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 // allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
// allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头 // allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
// credentials: true, // 允许携带凭据cookies等 // credentials: true, // 允许携带凭据(cookies等)
// }, // },
// jwt: { // jwt: {
// secret: 'YOONE2024!@abc', // secret: 'YOONE2024!@abc',
@ -140,5 +140,11 @@ export default {
user: 'info@canpouches.com', user: 'info@canpouches.com',
pass: 'WWqQ4aZq4Jrm9uwz', pass: 'WWqQ4aZq4Jrm9uwz',
}, },
} },
upload: {
// mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
mode: 'file',
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
whitelist: ['.csv'], // 支持的文件后缀
},
} as MidwayConfig; } as MidwayConfig;

View File

@ -16,6 +16,7 @@ import * as swagger from '@midwayjs/swagger';
import * as crossDomain from '@midwayjs/cross-domain'; import * as crossDomain from '@midwayjs/cross-domain';
import * as cron from '@midwayjs/cron'; import * as cron from '@midwayjs/cron';
import * as jwt from '@midwayjs/jwt'; import * as jwt from '@midwayjs/jwt';
import * as upload from '@midwayjs/upload';
import { USER_KEY } from './decorator/user.decorator'; import { USER_KEY } from './decorator/user.decorator';
import { SiteService } from './service/site.service'; import { SiteService } from './service/site.service';
import { AuthMiddleware } from './middleware/auth.middleware'; import { AuthMiddleware } from './middleware/auth.middleware';
@ -33,6 +34,7 @@ import { AuthMiddleware } from './middleware/auth.middleware';
crossDomain, crossDomain,
cron, cron,
jwt, jwt,
upload,
], ],
importConfigs: [join(__dirname, './config')], importConfigs: [join(__dirname, './config')],
}) })

View File

@ -86,7 +86,7 @@ export class AreaController {
} }
} }
@ApiOperation({ summary: '获取区域列表(分页)' }) @ApiOperation({ summary: '获取区域列表(分页)' })
@ApiOkResponse({ type: [Area], description: '区域列表' }) @ApiOkResponse({ type: [Area], description: '区域列表' })
@ApiExtension('x-pagination', { currentPage: 1, pageSize: 10, total: 100 }) @ApiExtension('x-pagination', { currentPage: 1, pageSize: 10, total: 100 })
@Get('/') @Get('/')

View File

@ -50,7 +50,7 @@ export class DictController {
*/ */
@Get('/:id') @Get('/:id')
async getDict(@Param('id') id: number) { async getDict(@Param('id') id: number) {
// 调用服务层方法并关联查询字典项 // 调用服务层方法,并关联查询字典项
return this.dictService.getDict({ id }, ['items']); return this.dictService.getDict({ id }, ['items']);
} }
@ -101,7 +101,7 @@ export class DictController {
/** /**
* *
* @param files * @param files
* @param body ID * @param body ,ID
*/ */
@Post('/item/import') @Post('/item/import')
@Validate() @Validate()

View File

@ -12,7 +12,7 @@ export class LocaleController {
/** /**
* *
* @param lang zh-CN, en-US * @param lang , zh-CN, en-US
* @returns JSON * @returns JSON
*/ */
@Get('/:lang') @Get('/:lang')
@ -20,7 +20,7 @@ export class LocaleController {
// 根据语言代码查找对应的字典 // 根据语言代码查找对应的字典
const dict = await this.dictService.getDict({ name: lang }, ['items']); const dict = await this.dictService.getDict({ name: lang }, ['items']);
// 如果字典不存在则返回空对象 // 如果字典不存在,则返回空对象
if (!dict) { if (!dict) {
return {}; return {};
} }

View File

@ -9,6 +9,7 @@ import {
Query, Query,
Controller, Controller,
} from '@midwayjs/core'; } from '@midwayjs/core';
import * as fs from 'fs';
import { ProductService } from '../service/product.service'; import { ProductService } from '../service/product.service';
import { errorResponse, successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto'; import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto';
@ -62,12 +63,14 @@ export class ProductController {
async getProductList( async getProductList(
@Query() query: QueryProductDTO @Query() query: QueryProductDTO
): Promise<ProductListRes> { ): Promise<ProductListRes> {
const { current = 1, pageSize = 10, name, brandId } = query; const { current = 1, pageSize = 10, name, brandId, sortField, sortOrder } = query;
try { try {
const data = await this.productService.getProductList( const data = await this.productService.getProductList(
{ current, pageSize }, { current, pageSize },
name, name,
brandId brandId,
sortField,
sortOrder
); );
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -87,14 +90,14 @@ export class ProductController {
} }
} }
// 中文注释:导出所有产品 CSV // 导出所有产品 CSV
@ApiOkResponse() @ApiOkResponse()
@Get('/export') @Get('/export')
@ContentType('text/csv') @ContentType('text/csv')
async exportProductsCSV() { async exportProductsCSV() {
try { try {
const csv = await this.productService.exportProductsCSV(); const csv = await this.productService.exportProductsCSV();
// 设置下载文件名(中文注释:附件形式) // 设置下载文件名(附件形式)
const date = new Date(); const date = new Date();
const pad = (n: number) => String(n).padStart(2, '0'); const pad = (n: number) => String(n).padStart(2, '0');
const name = `products-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}.csv`; const name = `products-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}.csv`;
@ -105,15 +108,26 @@ export class ProductController {
} }
} }
// 中文注释导入产品CSV 文件) // 导入产品(CSV 文件)
@ApiOkResponse() @ApiOkResponse()
@Post('/import') @Post('/import')
async importProductsCSV(@Files() files: any) { async importProductsCSV(@Files() files: any) {
try { try {
// 条件判断确保存在文件 // 条件判断:确保存在文件
const file = files?.[0]; const file = files?.[0];
if (!file?.data) return errorResponse('未接收到上传文件'); if (!file?.data) return errorResponse('未接收到上传文件');
const result = await this.productService.importProductsCSV(file.data);
// midway/upload file 模式下,data 是临时文件路径
let buffer = file.data;
if (typeof file.data === 'string') {
try {
buffer = fs.readFileSync(file.data);
} catch (err) {
return errorResponse('读取上传文件失败');
}
}
const result = await this.productService.importProductsCSV(buffer);
return successResponse(result); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -153,7 +167,7 @@ export class ProductController {
} }
} }
// 中文注释:获取产品的库存组成 // 获取产品的库存组成
@ApiOkResponse() @ApiOkResponse()
@Get('/:id/components') @Get('/:id/components')
async getProductComponents(@Param('id') id: number) { async getProductComponents(@Param('id') id: number) {
@ -165,7 +179,7 @@ export class ProductController {
} }
} }
// 中文注释:设置产品的库存组成(覆盖式) // 设置产品的库存组成(覆盖式)
@ApiOkResponse() @ApiOkResponse()
@Post('/:id/components') @Post('/:id/components')
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) { async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
@ -177,7 +191,7 @@ export class ProductController {
} }
} }
// 中文注释:根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存) // 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
@ApiOkResponse() @ApiOkResponse()
@Post('/:id/components/auto') @Post('/:id/components/auto')
async autoBindComponents(@Param('id') id: number) { async autoBindComponents(@Param('id') id: number) {
@ -204,7 +218,7 @@ export class ProductController {
// 通用属性接口分页列表 // 通用属性接口:分页列表
@ApiOkResponse() @ApiOkResponse()
@Get('/attribute') @Get('/attribute')
async getAttributeList( async getAttributeList(
@ -225,7 +239,7 @@ export class ProductController {
} }
} }
// 通用属性接口全部列表 // 通用属性接口:全部列表
@ApiOkResponse() @ApiOkResponse()
@Get('/attributeAll') @Get('/attributeAll')
async getAttributeAll(@Query('dictName') dictName: string) { async getAttributeAll(@Query('dictName') dictName: string) {
@ -237,7 +251,7 @@ export class ProductController {
} }
} }
// 通用属性接口创建 // 通用属性接口:创建
@ApiOkResponse() @ApiOkResponse()
@Post('/attribute') @Post('/attribute')
async createAttribute( async createAttribute(
@ -245,7 +259,7 @@ export class ProductController {
@Body() body: { title: string; name: string } @Body() body: { title: string; name: string }
) { ) {
try { try {
// 调用 getOrCreateAttribute 方法,如果不存在则创建,如果存在则返回 // 调用 getOrCreateAttribute 方法,如果不存在则创建,如果存在则返回
const data = await this.productService.getOrCreateAttribute(dictName, body.title, body.name); const data = await this.productService.getOrCreateAttribute(dictName, body.title, body.name);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -253,7 +267,7 @@ export class ProductController {
} }
} }
// 通用属性接口更新 // 通用属性接口:更新
@ApiOkResponse() @ApiOkResponse()
@Put('/attribute/:id') @Put('/attribute/:id')
async updateAttribute( async updateAttribute(
@ -277,7 +291,7 @@ export class ProductController {
} }
} }
// 通用属性接口删除 // 通用属性接口:删除
@ApiOkResponse({ type: BooleanRes }) @ApiOkResponse({ type: BooleanRes })
@Del('/attribute/:id') @Del('/attribute/:id')
async deleteAttribute(@Param('id') id: number) { async deleteAttribute(@Param('id') id: number) {
@ -289,12 +303,12 @@ export class ProductController {
} }
} }
// 兼容旧接口品牌 // 兼容旧接口:品牌
@ApiOkResponse() @ApiOkResponse()
@Get('/brandAll') @Get('/brandAll')
async compatBrandAll() { async compatBrandAll() {
try { try {
const data = await this.productService.getAttributeAll('brand'); // 中文注释:返回所有品牌字典项 const data = await this.productService.getAttributeAll('brand'); // 返回所有品牌字典项
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -305,7 +319,7 @@ export class ProductController {
@Get('/brands') @Get('/brands')
async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) { async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try { try {
const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 中文注释:分页品牌列表 const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 分页品牌列表
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -316,9 +330,9 @@ export class ProductController {
@Post('/brand') @Post('/brand')
async compatCreateBrand(@Body() body: { title: string; name: string }) { async compatCreateBrand(@Body() body: { title: string; name: string }) {
try { try {
const has = await this.productService.hasAttribute('brand', body.name); // 中文注释:唯一性校验 const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
if (has) return errorResponse('品牌已存在'); if (has) return errorResponse('品牌已存在');
const data = await this.productService.createAttribute('brand', body); // 中文注释:创建品牌字典项 const data = await this.productService.createAttribute('brand', body); // 创建品牌字典项
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -330,10 +344,10 @@ export class ProductController {
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) { async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
try { try {
if (body?.name) { if (body?.name) {
const has = await this.productService.hasAttribute('brand', body.name, id); // 中文注释:唯一性校验(排除自身) const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
if (has) return errorResponse('品牌已存在'); if (has) return errorResponse('品牌已存在');
} }
const data = await this.productService.updateAttribute(id, body); // 中文注释:更新品牌字典项 const data = await this.productService.updateAttribute(id, body); // 更新品牌字典项
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -344,14 +358,14 @@ export class ProductController {
@Del('/brand/:id') @Del('/brand/:id')
async compatDeleteBrand(@Param('id') id: number) { async compatDeleteBrand(@Param('id') id: number) {
try { try {
await this.productService.deleteAttribute(id); // 中文注释:删除品牌字典项 await this.productService.deleteAttribute(id); // 删除品牌字典项
return successResponse(true); return successResponse(true);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
// 兼容旧接口口味 // 兼容旧接口:口味
@ApiOkResponse() @ApiOkResponse()
@Get('/flavorsAll') @Get('/flavorsAll')
async compatFlavorsAll() { async compatFlavorsAll() {
@ -413,7 +427,7 @@ export class ProductController {
} }
} }
// 兼容旧接口规格 // 兼容旧接口:规格
@ApiOkResponse() @ApiOkResponse()
@Get('/strengthAll') @Get('/strengthAll')
async compatStrengthAll() { async compatStrengthAll() {
@ -475,7 +489,7 @@ export class ProductController {
} }
} }
// 兼容旧接口尺寸 // 兼容旧接口:尺寸
@ApiOkResponse() @ApiOkResponse()
@Get('/sizeAll') @Get('/sizeAll')
async compatSizeAll() { async compatSizeAll() {

View File

@ -176,7 +176,7 @@ export class StockController {
} }
} }
// 中文注释:检查某个 SKU 是否有库存(任一仓库数量大于 0 // 检查某个 SKU 是否有库存(任一仓库数量大于 0)
@ApiOkResponse({ type: BooleanRes }) @ApiOkResponse({ type: BooleanRes })
@Get('/has/:sku') @Get('/has/:sku')
async hasStock(@Param('sku') sku: string) { async hasStock(@Param('sku') sku: string) {
@ -190,7 +190,7 @@ export class StockController {
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
description: '更新库存(入库、出库、调整)', description: '更新库存(入库,出库,调整)',
}) })
@Post('/update') @Post('/update')
async updateStock(@Body() body: UpdateStockDTO) { async updateStock(@Body() body: UpdateStockDTO) {

View File

@ -10,7 +10,7 @@ export class SubscriptionController {
@Inject() @Inject()
subscriptionService: SubscriptionService; subscriptionService: SubscriptionService;
// 同步订阅根据站点 ID 拉取并更新本地订阅数据 // 同步订阅:根据站点 ID 拉取并更新本地订阅数据
@ApiOkResponse({ type: BooleanRes }) @ApiOkResponse({ type: BooleanRes })
@Post('/sync/:siteId') @Post('/sync/:siteId')
async sync(@Param('siteId') siteId: number) { async sync(@Param('siteId') siteId: number) {
@ -22,7 +22,7 @@ export class SubscriptionController {
} }
} }
// 订阅列表分页 + 筛选 // 订阅列表:分页 + 筛选
@ApiOkResponse({ type: SubscriptionListRes }) @ApiOkResponse({ type: SubscriptionListRes })
@Get('/list') @Get('/list')
async list(@Query() query: QuerySubscriptionDTO) { async list(@Query() query: QuerySubscriptionDTO) {

View File

@ -47,7 +47,7 @@ export class TemplateController {
/** /**
* @summary * @summary
* @description * @description ,
* @param templateData * @param templateData
*/ */
@ApiOkResponse({ type: Template, description: '成功创建模板' }) @ApiOkResponse({ type: Template, description: '成功创建模板' })

View File

@ -34,7 +34,7 @@ export class UserController {
}) })
@Post('/logout') @Post('/logout')
async logout() { async logout() {
// 可选在这里处理服务端缓存的 token 或 session // 可选:在这里处理服务端缓存的 token 或 session
return successResponse(true); return successResponse(true);
} }
@ -43,7 +43,7 @@ export class UserController {
async addUser(@Body() body: { username: string; password: string; remark?: string }) { async addUser(@Body() body: { username: string; password: string; remark?: string }) {
const { username, password, remark } = body; const { username, password, remark } = body;
try { try {
// 中文注释:新增用户(支持备注) // 新增用户(支持备注)
await this.userService.addUser(username, password, remark); await this.userService.addUser(username, password, remark);
return successResponse(true); return successResponse(true);
} catch (error) { } catch (error) {
@ -66,9 +66,9 @@ export class UserController {
} }
) { ) {
const { current = 1, pageSize = 10, remark, username, isActive, isSuper, isAdmin } = query; const { current = 1, pageSize = 10, remark, username, isActive, isSuper, isAdmin } = query;
// 中文注释:将字符串布尔转换为真实布尔 // 将字符串布尔转换为真实布尔
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true'); const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
// 中文注释:列表移除密码字段 // 列表移除密码字段
const { items, total } = await this.userService.listUsers(current, pageSize, { const { items, total } = await this.userService.listUsers(current, pageSize, {
remark, remark,
username, username,
@ -86,9 +86,9 @@ export class UserController {
@Post('/toggleActive') @Post('/toggleActive')
async toggleActive(@Body() body: { userId: number; isActive: boolean }) { async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
try { try {
// 中文注释:调用服务层更新启用状态 // 调用服务层更新启用状态
const data = await this.userService.toggleUserActive(body.userId, body.isActive); const data = await this.userService.toggleUserActive(body.userId, body.isActive);
// 中文注释:移除密码字段,保证安全 // 移除密码字段,保证安全
const { password, ...safe } = data as any; const { password, ...safe } = data as any;
return successResponse(safe); return successResponse(safe);
} catch (error) { } catch (error) {
@ -96,18 +96,18 @@ export class UserController {
} }
} }
// 中文注释:更新用户(支持用户名/密码/权限/角色更新) // 更新用户(支持用户名/密码/权限/角色更新)
@Post('/update/:id') @Post('/update/:id')
async updateUser( async updateUser(
@Body() body: { username?: string; password?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string }, @Body() body: { username?: string; password?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
@Query('id') id?: number @Query('id') id?: number
) { ) {
try { try {
// 条件判断:优先从路径参数获取 ID兼容生成的 API 文件为 POST /user/update/:id // 条件判断:优先从路径参数获取 ID(兼容生成的 API 文件为 POST /user/update/:id)
const userId = Number((this.ctx?.params?.id ?? id)); const userId = Number((this.ctx?.params?.id ?? id));
if (!userId) throw new Error('缺少用户ID'); if (!userId) throw new Error('缺少用户ID');
const data = await this.userService.updateUser(userId, body); const data = await this.userService.updateUser(userId, body);
// 中文注释:移除密码字段,保证安全 // 移除密码字段,保证安全
const { password, ...safe } = data as any; const { password, ...safe } = data as any;
return successResponse(safe); return successResponse(safe);
} catch (error) { } catch (error) {
@ -119,7 +119,7 @@ export class UserController {
@Get() @Get()
async getUser(@User() user) { async getUser(@User() user) {
try { try {
// 中文注释:详情移除密码字段 // 详情移除密码字段
const data = await this.userService.getUser(user.id); const data = await this.userService.getUser(user.id);
const { password, ...safe } = (data as any) || {}; const { password, ...safe } = (data as any) || {};
return successResponse(safe); return successResponse(safe);

View File

@ -33,7 +33,7 @@ export class WebhookController {
@Inject() @Inject()
private readonly siteService: SiteService; private readonly siteService: SiteService;
// 移除配置中的站点数组来源统一改为数据库 // 移除配置中的站点数组,来源统一改为数据库
@Get('/') @Get('/')
async test() { async test() {

View File

@ -25,7 +25,7 @@ import {
} from '../dto/reponse.dto'; } from '../dto/reponse.dto';
@Controller('/wp_product') @Controller('/wp_product')
export class WpProductController { export class WpProductController {
// 移除控制器内的配置站点引用统一由服务层处理站点数据 // 移除控制器内的配置站点引用,统一由服务层处理站点数据
@Inject() @Inject()
private readonly wpProductService: WpProductService; private readonly wpProductService: WpProductService;

View File

@ -149,7 +149,7 @@ export default class DictSeeder implements Seeder {
const formattedName = this.formatName(dictInfo.name); const formattedName = this.formatName(dictInfo.name);
let dict = await repo.findOne({ where: { name: formattedName } }); let dict = await repo.findOne({ where: { name: formattedName } });
if (!dict) { if (!dict) {
// 如果字典不存在则使用格式化后的 name 创建新字典 // 如果字典不存在,则使用格式化后的 name 创建新字典
dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn }); dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn });
} }
return dict; return dict;
@ -167,7 +167,7 @@ export default class DictSeeder implements Seeder {
const formattedName = this.formatName(item.name); const formattedName = this.formatName(item.name);
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } }); const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
if (!existingItem) { if (!existingItem) {
// 如果字典项不存在则使用格式化后的 name 创建新字典项 // 如果字典项不存在,则使用格式化后的 name 创建新字典项
await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, dict }); await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, dict });
} }
} }

View File

@ -4,14 +4,14 @@ import { Template } from '../../entity/template.entity';
/** /**
* @class TemplateSeeder * @class TemplateSeeder
* @description * @description ,.
*/ */
export default class TemplateSeeder implements Seeder { export default class TemplateSeeder implements Seeder {
/** /**
* @method run * @method run
* @description product_sku * @description . product_sku ,.
* @param {DataSource} dataSource - repository * @param {DataSource} dataSource - , repository.
* @param {SeederFactoryManager} factoryManager - Seeder * @param {SeederFactoryManager} factoryManager - Seeder .
*/ */
public async run( public async run(
dataSource: DataSource, dataSource: DataSource,
@ -25,7 +25,7 @@ export default class TemplateSeeder implements Seeder {
where: { name: 'product_sku' }, where: { name: 'product_sku' },
}); });
// 如果模板不存在则创建并保存 // 如果模板不存在,则创建并保存
if (!existingTemplate) { if (!existingTemplate) {
const template = new Template(); const template = new Template();
template.name = 'product_sku'; template.name = 'product_sku';

View File

@ -23,7 +23,7 @@ export class QueryAreaDTO {
@Rule(RuleType.number().integer().min(1).default(10)) @Rule(RuleType.number().integer().min(1).default(10))
pageSize?: number; pageSize?: number;
@ApiProperty({ description: '关键词(名称或编码)', required: false }) @ApiProperty({ description: '关键词(名称或编码)', required: false })
@Rule(RuleType.string()) @Rule(RuleType.string())
keyword?: string; keyword?: string;
} }

View File

@ -8,7 +8,7 @@ export type PackagingType =
// | PackagingCourierPak // | PackagingCourierPak
// | PackagingEnvelope; // | PackagingEnvelope;
// 定义包装类型的枚举用于 API 文档描述 // 定义包装类型的枚举,用于 API 文档描述
export enum PackagingTypeEnum { export enum PackagingTypeEnum {
Pallet = 'pallet', Pallet = 'pallet',
Package = 'package', Package = 'package',

View File

@ -92,7 +92,7 @@ export class QueryOrderDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
payment_method: string; payment_method: string;
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' }) @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
@Rule(RuleType.bool().default(false)) @Rule(RuleType.bool().default(false))
isSubscriptionOnly?: boolean; isSubscriptionOnly?: boolean;
} }

View File

@ -38,6 +38,10 @@ export class CreateProductDTO {
@Rule(RuleType.string().required().empty({ message: '产品名称不能为空' })) @Rule(RuleType.string().required().empty({ message: '产品名称不能为空' }))
name: string; name: string;
@ApiProperty({ description: '产品中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
nameCn?: string;
@ApiProperty({ example: '产品描述', description: '产品描述' }) @ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string()) @Rule(RuleType.string())
description: string; description: string;
@ -50,7 +54,7 @@ export class CreateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
categoryId?: number; categoryId?: number;
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等) // 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
@ApiProperty({ description: '属性列表', type: 'array' }) @ApiProperty({ description: '属性列表', type: 'array' })
@Rule(RuleType.array().required()) @Rule(RuleType.array().required())
attributes: AttributeInputDTO[]; attributes: AttributeInputDTO[];
@ -65,12 +69,14 @@ export class CreateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
promotionPrice?: number; promotionPrice?: number;
// 中文注释:商品类型(默认 singlebundle 需手动设置组成)
// 商品类型(默认 single; bundle 需手动设置组成)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false }) @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
@Rule(RuleType.string().valid('single', 'bundle').default('single')) @Rule(RuleType.string().valid('single', 'bundle').default('single'))
type?: string; type?: string;
// 中文注释:仅当 type 为 'bundle' 时,才需要提供 components // 仅当 type 为 'bundle' 时,才需要提供 components
@ApiProperty({ description: '产品组成', type: 'array', required: false }) @ApiProperty({ description: '产品组成', type: 'array', required: false })
@Rule( @Rule(
RuleType.array() RuleType.array()
@ -96,6 +102,10 @@ export class UpdateProductDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
name?: string; name?: string;
@ApiProperty({ description: '产品中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
nameCn?: string;
@ApiProperty({ example: '产品描述', description: '产品描述' }) @ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string()) @Rule(RuleType.string())
description?: string; description?: string;
@ -118,12 +128,14 @@ export class UpdateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
promotionPrice?: number; promotionPrice?: number;
// 属性更新(中文注释:可选,支持增量替换指定字典的属性项)
// 属性更新(可选, 支持增量替换指定字典的属性项)
@ApiProperty({ description: '属性列表', type: 'array', required: false }) @ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array()) @Rule(RuleType.array())
attributes?: AttributeInputDTO[]; attributes?: AttributeInputDTO[];
// 中文注释商品类型single 或 bundle // 商品类型(single 或 bundle)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false }) @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
@Rule(RuleType.string().valid('single', 'bundle')) @Rule(RuleType.string().valid('single', 'bundle'))
type?: string; type?: string;
@ -165,6 +177,14 @@ export class QueryProductDTO {
@ApiProperty({ description: '品牌ID', required: false }) @ApiProperty({ description: '品牌ID', required: false })
@Rule(RuleType.number()) @Rule(RuleType.number())
brandId?: number; brandId?: number;
@ApiProperty({ description: '排序字段', required: false })
@Rule(RuleType.string())
sortField?: string;
@ApiProperty({ description: '排序方式', required: false })
@Rule(RuleType.string().valid('ascend', 'descend'))
sortOrder?: string;
} }
/** /**

View File

@ -148,7 +148,7 @@ export class PaymentMethodListRes extends SuccessArrayWrapper(
PaymentMethodDTO PaymentMethodDTO
) {} ) {}
// 订阅分页数据(列表 + 总数等分页信息) // 订阅分页数据(列表 + 总数等分页信息)
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {} export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
// 订阅分页返回数据(统一成功包装) // 订阅分页返回数据(统一成功包装)
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {} export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}

View File

@ -38,6 +38,8 @@ export class CreateSiteDTO {
consumerKey?: string; consumerKey?: string;
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
consumerSecret?: string; consumerSecret?: string;
@Rule(RuleType.string().optional())
token?: string;
@Rule(RuleType.string()) @Rule(RuleType.string())
name: string; name: string;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
@ -59,6 +61,8 @@ export class UpdateSiteDTO {
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
consumerSecret?: string; consumerSecret?: string;
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
token?: string;
@Rule(RuleType.string().optional())
name?: string; name?: string;
@Rule(RuleType.boolean().optional()) @Rule(RuleType.boolean().optional())
isDisabled?: boolean; isDisabled?: boolean;

View File

@ -30,7 +30,7 @@ export class QueryStockDTO {
@Rule(RuleType.number().allow(null)) @Rule(RuleType.number().allow(null))
sortPointId?: number; sortPointId?: number;
@ApiProperty({ description: '排序对象格式如 { productName: "asc", sku: "desc" }', required: false }) @ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }', required: false })
@Rule(RuleType.object().allow(null)) @Rule(RuleType.object().allow(null))
order?: Record<string, 'asc' | 'desc'>; order?: Record<string, 'asc' | 'desc'>;
} }

View File

@ -2,9 +2,9 @@ import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate'; import { Rule, RuleType } from '@midwayjs/validate';
import { SubscriptionStatus } from '../enums/base.enum'; import { SubscriptionStatus } from '../enums/base.enum';
// 订阅列表查询参数(分页与筛选) // 订阅列表查询参数(分页与筛选)
export class QuerySubscriptionDTO { export class QuerySubscriptionDTO {
// 当前页码(从 1 开始) // 当前页码(从 1 开始)
@ApiProperty({ example: 1, description: '页码' }) @ApiProperty({ example: 1, description: '页码' })
@Rule(RuleType.number().default(1)) @Rule(RuleType.number().default(1))
current: number; current: number;
@ -14,23 +14,23 @@ export class QuerySubscriptionDTO {
@Rule(RuleType.number().default(10)) @Rule(RuleType.number().default(10))
pageSize: number; pageSize: number;
// 站点 ID(可选) // 站点 ID(可选)
@ApiProperty({ description: '站点ID' }) @ApiProperty({ description: '站点ID' })
@Rule(RuleType.string().allow('')) @Rule(RuleType.string().allow(''))
siteId: string; siteId: string;
// 订阅状态筛选(可选),支持枚举值 // 订阅状态筛选(可选),支持枚举值
@ApiProperty({ description: '订阅状态', enum: SubscriptionStatus }) @ApiProperty({ description: '订阅状态', enum: SubscriptionStatus })
@Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow('')) @Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow(''))
status: SubscriptionStatus | ''; status: SubscriptionStatus | '';
// 客户邮箱(模糊匹配,可选) // 客户邮箱(模糊匹配,可选)
@ApiProperty({ description: '客户邮箱' }) @ApiProperty({ description: '客户邮箱' })
@Rule(RuleType.string().allow('')) @Rule(RuleType.string().allow(''))
customer_email: string; customer_email: string;
// 关键字订阅ID、邮箱等模糊匹配可选 // 关键字(订阅ID,邮箱等,模糊匹配,可选)
@ApiProperty({ description: '关键字订阅ID、邮箱等' }) @ApiProperty({ description: '关键字(订阅ID,邮箱等)' })
@Rule(RuleType.string().allow('')) @Rule(RuleType.string().allow(''))
keyword: string; keyword: string;
} }

View File

@ -178,7 +178,7 @@ export class Order {
@ApiProperty() @ApiProperty()
@Column({ @Column({
type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT
nullable: true, // 可选是否允许为 NULL nullable: true, // 可选:是否允许为 NULL
}) })
@Expose() @Expose()
customer_note: string; customer_note: string;

View File

@ -79,17 +79,17 @@ export class OrderItem {
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
tax_class?: string; // 税类(来自 line_items.tax_class tax_class?: string; // 税类(来自 line_items.tax_class)
@ApiProperty() @ApiProperty()
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@Expose() @Expose()
taxes?: any[]; // 税明细(来自 line_items.taxes数组 taxes?: any[]; // 税明细(来自 line_items.taxes,数组)
@ApiProperty() @ApiProperty()
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@Expose() @Expose()
meta_data?: any[]; // 行项目元数据(包含订阅相关键值) meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@ -99,7 +99,7 @@ export class OrderItem {
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供) global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供)
@ApiProperty() @ApiProperty()
@Column('decimal', { precision: 10, scale: 2 }) @Column('decimal', { precision: 10, scale: 2 })
@ -109,17 +109,17 @@ export class OrderItem {
@ApiProperty() @ApiProperty()
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@Expose() @Expose()
image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src)
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
parent_name?: string; // 父商品名称(组合/捆绑时可能使用) parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
bundled_by?: string; // 捆绑来源标识bundled_by bundled_by?: string; // 捆绑来源标识(bundled_by)
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@ -129,7 +129,7 @@ export class OrderItem {
@ApiProperty() @ApiProperty()
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@Expose() @Expose()
bundled_items?: any[]; // 捆绑项列表(数组) bundled_items?: any[]; // 捆绑项列表(数组)
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',

View File

@ -63,9 +63,7 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number; promotionPrice: number;
@ApiProperty({ description: '库存', example: 100 })
@Column({ default: 0 })
stock: number;
// 分类关联 // 分类关联
@ -79,7 +77,7 @@ export class Product {
@JoinTable() @JoinTable()
attributes: DictItem[]; attributes: DictItem[];
// 中文注释:产品的库存组成,一对多关系(使用独立表) // 产品的库存组成,一对多关系(使用独立表)
@ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true }) @ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true }) @OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
components: ProductStockComponent[]; components: ProductStockComponent[];

View File

@ -20,7 +20,7 @@ export class ProductStockComponent {
@Column({ type: 'int', default: 1 }) @Column({ type: 'int', default: 1 })
quantity: number; quantity: number;
// 中文注释:多对一,组件隶属于一个产品 // 多对一,组件隶属于一个产品
@ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' }) @ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
product: Product; product: Product;

View File

@ -22,7 +22,7 @@ export class Site {
name: string; name: string;
@Column({ length: 32, default: 'woocommerce' }) @Column({ length: 32, default: 'woocommerce' })
type: string; // 平台类型woocommerce | shopyy type: string; // 平台类型:woocommerce | shopyy
@Column({ length: 64, nullable: true }) @Column({ length: 64, nullable: true })
skuPrefix: string; skuPrefix: string;

View File

@ -12,68 +12,68 @@ import { SubscriptionStatus } from '../enums/base.enum';
@Entity('subscription') @Entity('subscription')
@Exclude() @Exclude()
export class Subscription { export class Subscription {
// 本地主键自增 ID // 本地主键,自增 ID
@ApiProperty() @ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
@Expose() @Expose()
id: number; id: number;
// 站点唯一标识用于区分不同来源站点 // 站点唯一标识,用于区分不同来源站点
@ApiProperty({ description: '来源站点唯一标识' }) @ApiProperty({ description: '来源站点唯一标识' })
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
siteId: number; siteId: number;
// WooCommerce 订阅的原始 ID(字符串化),用于幂等更新 // WooCommerce 订阅的原始 ID(字符串化),用于幂等更新
@ApiProperty({ description: 'WooCommerce 订阅 ID' }) @ApiProperty({ description: 'WooCommerce 订阅 ID' })
@Column() @Column()
@Expose() @Expose()
externalSubscriptionId: string; externalSubscriptionId: string;
// 订阅状态active/cancelled/on-hold 等) // 订阅状态(active/cancelled/on-hold 等)
@ApiProperty({ type: SubscriptionStatus }) @ApiProperty({ type: SubscriptionStatus })
@Column({ type: 'enum', enum: SubscriptionStatus }) @Column({ type: 'enum', enum: SubscriptionStatus })
@Expose() @Expose()
status: SubscriptionStatus; status: SubscriptionStatus;
// 货币代码例如 USD/CAD // 货币代码,例如 USD/CAD
@ApiProperty() @ApiProperty()
@Column({ default: '' }) @Column({ default: '' })
@Expose() @Expose()
currency: string; currency: string;
// 总金额保留两位小数 // 总金额,保留两位小数
@ApiProperty() @ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 }) @Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose() @Expose()
total: number; total: number;
// 计费周期day/week/month/year // 计费周期(day/week/month/year)
@ApiProperty({ description: '计费周期 e.g. day/week/month/year' }) @ApiProperty({ description: '计费周期 e.g. day/week/month/year' })
@Column({ default: '' }) @Column({ default: '' })
@Expose() @Expose()
billing_period: string; billing_period: string;
// 计费周期间隔(例如 1/3/12 // 计费周期间隔(例如 1/3/12)
@ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' }) @ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' })
@Column({ type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
@Expose() @Expose()
billing_interval: number; billing_interval: number;
// 客户 IDWooCommerce 用户 ID // 客户 ID(WooCommerce 用户 ID)
@ApiProperty() @ApiProperty()
@Column({ type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
@Expose() @Expose()
customer_id: number; customer_id: number;
// 客户邮箱(从 billing.email 或 customer_email 提取) // 客户邮箱(从 billing.email 或 customer_email 提取)
@ApiProperty() @ApiProperty()
@Column({ default: '' }) @Column({ default: '' })
@Expose() @Expose()
customer_email: string; customer_email: string;
// 父订单/订阅 ID(如有) // 父订单/订阅 ID(如有)
@ApiProperty({ description: '父订单/父订阅ID(如有)' }) @ApiProperty({ description: '父订单/父订阅ID(如有)' })
@Column({ type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
@Expose() @Expose()
parent_id: number; parent_id: number;
@ -102,25 +102,25 @@ export class Subscription {
@Expose() @Expose()
end_date: Date; end_date: Date;
// 商品项(订阅行项目) // 商品项(订阅行项目)
@ApiProperty() @ApiProperty()
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@Expose() @Expose()
line_items: any[]; line_items: any[];
// 额外元数据(键值对) // 额外元数据(键值对)
@ApiProperty() @ApiProperty()
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@Expose() @Expose()
meta_data: any[]; meta_data: any[];
// 创建时间(数据库自动生成) // 创建时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true }) @ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
@CreateDateColumn() @CreateDateColumn()
@Expose() @Expose()
createdAt: Date; createdAt: Date;
// 更新时间(数据库自动生成) // 更新时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true }) @ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
@UpdateDateColumn() @UpdateDateColumn()
@Expose() @Expose()

View File

@ -15,10 +15,10 @@ export class User {
password: string; password: string;
// @Column() // 默认角色为管理员 // @Column() // 默认角色为管理员
// roleId: number; // 角色 (如admin, editor, viewer) // roleId: number; // 角色 (如:admin, editor, viewer)
@Column({ type: 'simple-array', nullable: true }) @Column({ type: 'simple-array', nullable: true })
permissions: string[]; // 自定义权限 (如['user:add', 'user:edit']) permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
@Column({ default: false }) @Column({ default: false })
isSuper: boolean; // 超级管理员 isSuper: boolean; // 超级管理员
@ -29,7 +29,7 @@ export class User {
@Column({ default: true }) @Column({ default: true })
isActive: boolean; // 用户是否启用 isActive: boolean; // 用户是否启用
// 中文注释:备注字段(可选) // 备注字段(可选)
@Column({ nullable: true }) @Column({ nullable: true })
remark?: string; remark?: string;
} }

View File

@ -7,7 +7,7 @@ export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
return async (ctx: Context, next: NextFunction) => { return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑 // 控制器前执行的逻辑
const startTime = Date.now(); const startTime = Date.now();
// 执行下一个 Web 中间件最后执行到控制器 // 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值 // 这里可以拿到下一个中间件或者控制器的返回值
const result = await next(); const result = await next();
// 控制器之后执行的逻辑 // 控制器之后执行的逻辑

View File

@ -36,7 +36,7 @@ export class AreaService {
select: 'official', select: 'official',
}); });
// 如果找不到对应的国家则抛出错误 // 如果找不到对应的国家,则抛出错误
if (!name) { if (!name) {
throw new Error(`无效的国家代码: ${createAreaDTO.code}`); throw new Error(`无效的国家代码: ${createAreaDTO.code}`);
} }
@ -53,7 +53,7 @@ export class AreaService {
return null; return null;
} }
// 如果 code 发生变化则更新 name // 如果 code 发生变化,则更新 name
if (updateAreaDTO.code && updateAreaDTO.code !== area.code) { if (updateAreaDTO.code && updateAreaDTO.code !== area.code) {
const name = countries.getName(updateAreaDTO.code, 'zh', { const name = countries.getName(updateAreaDTO.code, 'zh', {
select: 'official', select: 'official',

View File

@ -57,7 +57,7 @@ export class CanadaPostService {
return builder.buildObject(xmlObj); return builder.buildObject(xmlObj);
} }
// 默认直接构建(用于 createShipment 这类已有完整结构) // 默认直接构建(用于 createShipment 这类已有完整结构)
return builder.buildObject(data); return builder.buildObject(data);
} }

View File

@ -100,7 +100,7 @@ export class DictService {
} }
return this.dictModel.findOne({ where, relations }); return this.dictModel.findOne({ where, relations });
} }
// 获取字典列表支持按标题搜索 // 获取字典列表,支持按标题搜索
async getDicts(options: { title?: string; name?: string; }) { async getDicts(options: { title?: string; name?: string; }) {
const where = { const where = {
title: options.title ? Like(`%${options.title}%`) : undefined, title: options.title ? Like(`%${options.title}%`) : undefined,
@ -135,7 +135,7 @@ export class DictService {
return result.affected > 0; return result.affected > 0;
} }
// 获取字典项列表支持按 dictId 过滤 // 获取字典项列表,支持按 dictId 过滤
async getDictItems(params: { dictId?: number; name?: string; title?: string; }) { async getDictItems(params: { dictId?: number; name?: string; title?: string; }) {
const { dictId, name, title } = params; const { dictId, name, title } = params;
const where: any = {}; const where: any = {};
@ -150,11 +150,11 @@ export class DictService {
where.title = Like(`%${title}%`); where.title = Like(`%${title}%`);
} }
// 如果提供了 dictId则只返回该字典下的项 // 如果提供了 dictId,则只返回该字典下的项
if (params.dictId) { if (params.dictId) {
return this.dictItemModel.find({ where }); return this.dictItemModel.find({ where });
} }
// 否则返回所有字典项 // 否则,返回所有字典项
return this.dictItemModel.find(); return this.dictItemModel.find();
} }
@ -191,7 +191,7 @@ export class DictService {
async getDictItemsByDictName(dictName: string) { async getDictItemsByDictName(dictName: string) {
// 查找字典 // 查找字典
const dict = await this.dictModel.findOne({ where: { name: dictName } }); const dict = await this.dictModel.findOne({ where: { name: dictName } });
// 如果字典不存在则返回空数组 // 如果字典不存在,则返回空数组
if (!dict) { if (!dict) {
return []; return [];
} }

View File

@ -228,7 +228,7 @@ export class LogisticsService {
async removeShipment(shipmentId: number) { async removeShipment(shipmentId: number) {
try { try {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId }); const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
if (shipment.state !== '190') { // todo写常数 if (shipment.state !== '190') { // todo,写常数
throw new Error('订单当前状态无法删除'); throw new Error('订单当前状态无法删除');
} }
const order: Order = await this.orderModel.findOneBy({ id: shipment.order_id }); const order: Order = await this.orderModel.findOneBy({ id: shipment.order_id });
@ -347,7 +347,7 @@ export class LogisticsService {
// 添加运单 // 添加运单
resShipmentOrder = await this.uniExpressService.createShipment(reqBody); resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
// 记录物流信息并将订单状态转到完成 // 记录物流信息,并将订单状态转到完成
if (resShipmentOrder.status === 'SUCCESS') { if (resShipmentOrder.status === 'SUCCESS') {
order.orderStatus = ErpOrderStatus.COMPLETED; order.orderStatus = ErpOrderStatus.COMPLETED;
} else { } else {
@ -359,7 +359,7 @@ export class LogisticsService {
await dataSource.transaction(async manager => { await dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order); const orderRepo = manager.getRepository(Order);
const shipmentRepo = manager.getRepository(Shipment); const shipmentRepo = manager.getRepository(Shipment);
const tracking_provider = 'UniUni'; // todo: id未确定后写进常数 const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
// 同步物流信息到woocommerce // 同步物流信息到woocommerce
const site = await this.siteService.get(Number(order.siteId), true); const site = await this.siteService.get(Number(order.siteId), true);
@ -414,7 +414,7 @@ export class LogisticsService {
if (resShipmentOrder.status === 'SUCCESS') { if (resShipmentOrder.status === 'SUCCESS') {
await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno); await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno);
} }
throw new Error(`上游请求错误${error}`); throw new Error(`上游请求错误:${error}`);
} }
} }
@ -558,7 +558,7 @@ export class LogisticsService {
}, },
}); });
// 从数据库批量获取站点信息构建映射以避免 N+1 查询 // 从数据库批量获取站点信息,构建映射以避免 N+1 查询
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
const siteMap = new Map(sites.map((s: any) => [s.id, s.name])); const siteMap = new Map(sites.map((s: any) => [s.id, s.name]));
@ -602,7 +602,7 @@ export class LogisticsService {
values.push(stockPointId); values.push(stockPointId);
} }
// todo增加订单号搜索 // todo,增加订单号搜索
if (externalOrderId) { if (externalOrderId) {
whereClause += ' AND o.externalOrderId = ?'; whereClause += ' AND o.externalOrderId = ?';
values.push(externalOrderId); values.push(externalOrderId);

View File

@ -119,7 +119,7 @@ export class OrderService {
[OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
} }
// 由于 wordpress 订单状态和 我们的订单状态 不一致需要做转换 // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
async autoUpdateOrderStatus(siteId: number, order: any) { async autoUpdateOrderStatus(siteId: number, order: any) {
console.log('更新订单状态', order) console.log('更新订单状态', order)
// 其他状态保持不变 // 其他状态保持不变
@ -130,14 +130,14 @@ export class OrderService {
} }
try { try {
const site = await this.siteService.get(siteId); const site = await this.siteService.get(siteId);
// 将订单状态同步到 WooCommerce然后切换至下一状态 // 将订单状态同步到 WooCommerce,然后切换至下一状态
await this.wpService.updateOrder(site, String(order.id), { status: order.status }); await this.wpService.updateOrder(site, String(order.id), { status: order.status });
order.status = this.orderAutoNextStatusMap[originStatus]; order.status = this.orderAutoNextStatusMap[originStatus];
} catch (error) { } catch (error) {
console.error('更新订单状态失败,原因为:', error) console.error('更新订单状态失败,原因为:', error)
} }
} }
// wordpress 发来 // wordpress 发来,
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
let { let {
line_items, line_items,
@ -275,7 +275,7 @@ export class OrderService {
'PENDING_RESHIPMENT', 'PENDING_RESHIPMENT',
'PENDING_REFUND', 'PENDING_REFUND',
]; ];
// 如果当前 ERP 状态不可覆盖则禁止更新 // 如果当前 ERP 状态不可覆盖,则禁止更新
return !nonOverridableStatuses.includes(currentErpStatus); return !nonOverridableStatuses.includes(currentErpStatus);
} }
@ -732,7 +732,7 @@ export class OrderService {
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_created <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
// 支付方式筛选使用参数化避免SQL注入 // 支付方式筛选(使用参数化,避免SQL注入)
if (payment_method) { if (payment_method) {
sqlQuery += ` AND o.payment_method LIKE ?`; sqlQuery += ` AND o.payment_method LIKE ?`;
totalQuery += ` AND o.payment_method LIKE ?`; totalQuery += ` AND o.payment_method LIKE ?`;
@ -764,7 +764,7 @@ export class OrderService {
} }
} }
// 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储) // 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储)
if (isSubscriptionOnly) { if (isSubscriptionOnly) {
const subCond = ` const subCond = `
AND ( AND (
@ -862,7 +862,7 @@ export class OrderService {
customer_email: `%${customer_email}%`, customer_email: `%${customer_email}%`,
}); });
// 🔥 关键字搜索检查 order_item.name 是否包含 keyword // 🔥 关键字搜索:检查 order_item.name 是否包含 keyword
if (keyword) { if (keyword) {
query.andWhere( query.andWhere(
`EXISTS ( `EXISTS (
@ -1025,7 +1025,7 @@ export class OrderService {
} }
// ------------------------- // -------------------------
// 4. 总量统计(时间段 + siteId // 4. 总量统计(时间段 + siteId)
// ------------------------- // -------------------------
const totalParams: any[] = [startDate, endDate]; const totalParams: any[] = [startDate, endDate];
const yooneParams: any[] = [startDate, endDate]; const yooneParams: any[] = [startDate, endDate];
@ -1304,7 +1304,7 @@ export class OrderService {
// 关联数据:订阅与相关订单(用于前端关联展示) // 关联数据:订阅与相关订单(用于前端关联展示)
let relatedList: any[] = []; let relatedList: any[] = [];
try { try {
const related = await this.getRelatedByOrder(id); const related = await this.getRelatedByOrder(id);
@ -1330,7 +1330,7 @@ export class OrderService {
return { return {
...order, ...order,
name: site?.name, name: site?.name,
// Site 实体无邮箱字段这里返回空字符串保持兼容 // Site 实体无邮箱字段,这里返回空字符串保持兼容
email: '', email: '',
items, items,
sales, sales,
@ -1394,7 +1394,7 @@ export class OrderService {
]), ]),
}, },
}); });
// 批量获取订单涉及的站点名称避免使用配置文件 // 批量获取订单涉及的站点名称,避免使用配置文件
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name])); const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name]));
@ -1459,7 +1459,7 @@ export class OrderService {
async createOrder(data: Record<string, any>) { async createOrder(data: Record<string, any>) {
// 从数据中解构出需要用的属性 // 从数据中解构出需要用的属性
const { siteId, sales, total, billing, customer_email, billing_phone } = data; const { siteId, sales, total, billing, customer_email, billing_phone } = data;
// 如果没有 siteId则抛出错误 // 如果没有 siteId,则抛出错误
if (!siteId) { if (!siteId) {
throw new Error('siteId is required'); throw new Error('siteId is required');
} }
@ -1573,11 +1573,11 @@ export class OrderService {
}); });
if (transactionError !== undefined) { if (transactionError !== undefined) {
throw new Error(`更新物流信息错误${transactionError.message}`); throw new Error(`更新物流信息错误:${transactionError.message}`);
} }
return true; return true;
} catch (error) { } catch (error) {
throw new Error(`更新发货产品失败${error.message}`); throw new Error(`更新发货产品失败:${error.message}`);
} }
} }
@ -1658,11 +1658,11 @@ export class OrderService {
}); });
if (transactionError !== undefined) { if (transactionError !== undefined) {
throw new Error(`更新物流信息错误${transactionError.message}`); throw new Error(`更新物流信息错误:${transactionError.message}`);
} }
return true; return true;
} catch (error) { } catch (error) {
throw new Error(`更新发货产品失败${error.message}`); throw new Error(`更新发货产品失败:${error.message}`);
} }
} }

View File

@ -91,7 +91,7 @@ export class ProductService {
return []; return [];
} }
// 格式化返回方便前端使用 // 格式化返回,方便前端使用
return category.attributes.map(attr => ({ return category.attributes.map(attr => ({
id: attr.id, id: attr.id,
dictId: attr.attributeDict.id, dictId: attr.attributeDict.id,
@ -174,7 +174,7 @@ export class ProductService {
// where.nameCn = Like(`%${name}%`) // where.nameCn = Like(`%${name}%`)
// } // }
// where.sku = Not(IsNull()); // where.sku = Not(IsNull());
// // 查询 SKU 不为空且 name 包含关键字的产品最多返回 50 条 // // 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
// return this.productModel.find({ // return this.productModel.find({
// where, // where,
// take: 50, // take: 50,
@ -193,7 +193,7 @@ export class ProductService {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
const conditions: string[] = []; const conditions: string[] = [];
// 英文名关键词全部匹配AND // 英文名关键词全部匹配(AND)
if (nameFilter.length > 0) { if (nameFilter.length > 0) {
const nameConds = nameFilter.map((word, index) => { const nameConds = nameFilter.map((word, index) => {
const key = `name${index}`; const key = `name${index}`;
@ -230,7 +230,9 @@ export class ProductService {
async getProductList( async getProductList(
pagination: PaginationParams, pagination: PaginationParams,
name?: string, name?: string,
brandId?: number brandId?: number,
sortField?: string,
sortOrder?: string
): Promise<ProductPaginatedResponse> { ): Promise<ProductPaginatedResponse> {
const qb = this.productModel const qb = this.productModel
.createQueryBuilder('product') .createQueryBuilder('product')
@ -238,7 +240,7 @@ export class ProductService {
.leftJoinAndSelect('attribute.dict', 'dict') .leftJoinAndSelect('attribute.dict', 'dict')
.leftJoinAndSelect('product.category', 'category'); .leftJoinAndSelect('product.category', 'category');
// 模糊搜索 name支持多个关键词 // 模糊搜索 name,支持多个关键词
const nameFilter = name ? name.split(' ').filter(Boolean) : []; const nameFilter = name ? name.split(' ').filter(Boolean) : [];
if (nameFilter.length > 0) { if (nameFilter.length > 0) {
const nameConditions = nameFilter const nameConditions = nameFilter
@ -266,6 +268,17 @@ export class ProductService {
}); });
} }
// 排序
if (sortField && sortOrder) {
const order = sortOrder === 'ascend' ? 'ASC' : 'DESC';
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
if (allowedSortFields.includes(sortField)) {
qb.orderBy(`product.${sortField}`, order);
}
} else {
qb.orderBy('product.createdAt', 'DESC');
}
// 分页 // 分页
qb.skip((pagination.current - 1) * pagination.pageSize).take( qb.skip((pagination.current - 1) * pagination.pageSize).take(
pagination.pageSize pagination.pageSize
@ -273,17 +286,17 @@ export class ProductService {
const [items, total] = await qb.getManyAndCount(); const [items, total] = await qb.getManyAndCount();
// 中文注释:根据类型填充组成信息 // 根据类型填充组成信息
for (const product of items) { for (const product of items) {
if (product.type === 'single') { if (product.type === 'single') {
// 中文注释:单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成 // 单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
const component = new ProductStockComponent(); const component = new ProductStockComponent();
component.productId = product.id; component.productId = product.id;
component.sku = product.sku; component.sku = product.sku;
component.quantity = 1; component.quantity = 1;
product.components = [component]; product.components = [component];
} else { } else {
// 中文注释:混装商品返回持久化的 SKU 组成 // 混装商品返回持久化的 SKU 组成
product.components = await this.productStockComponentModel.find({ product.components = await this.productStockComponentModel.find({
where: { productId: product.id }, where: { productId: product.id },
}); });
@ -315,7 +328,7 @@ export class ProductService {
where: { name: nameForLookup, dict: { id: dict.id } }, where: { name: nameForLookup, dict: { id: dict.id } },
}); });
// 如果字典项不存在则创建 // 如果字典项不存在,则创建
if (!item) { if (!item) {
item = new DictItem(); item = new DictItem();
item.title = itemTitle; item.title = itemTitle;
@ -327,14 +340,15 @@ export class ProductService {
return item; return item;
} }
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const { name, description, attributes, sku, price, categoryId } = createProductDTO;
// 条件判断(中文注释:校验属性输入) async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const { attributes, sku, categoryId } = createProductDTO;
// 条件判断(校验属性输入)
if (!Array.isArray(attributes) || attributes.length === 0) { if (!Array.isArray(attributes) || attributes.length === 0) {
// 如果提供了 categoryId 但没有 attributes初始化为空数组 // 如果提供了 categoryId 但没有 attributes,初始化为空数组
if (!attributes && categoryId) { if (!attributes && categoryId) {
// 继续执行下面会处理 categoryId // 继续执行,下面会处理 categoryId
} else { } else {
throw new Error('属性列表不能为空'); throw new Error('属性列表不能为空');
} }
@ -342,34 +356,42 @@ export class ProductService {
const safeAttributes = attributes || []; const safeAttributes = attributes || [];
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项) // 解析属性输入(按 id 或 dictName 创建/关联字典项)
const resolvedAttributes: DictItem[] = []; const resolvedAttributes: DictItem[] = [];
let categoryItem: Category | null = null; let categoryItem: Category | null = null;
// 如果提供了 categoryId设置分类 // 如果提供了 categoryId,设置分类
if (categoryId) { if (categoryId) {
categoryItem = await this.categoryModel.findOne({ where: { id: categoryId } }); categoryItem = await this.categoryModel.findOne({ where: { id: categoryId } });
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`); if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
} }
for (const attr of safeAttributes) { for (const attr of safeAttributes) {
// 中文注释:如果属性是分类,特殊处理 // 如果属性是分类,特殊处理
if (attr.dictName === 'category') { if (attr.dictName === 'category') {
if (attr.id) { if (attr.id) {
categoryItem = await this.categoryModel.findOneBy({ id: attr.id }); categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
} else if (attr.name) { } else if (attr.name) {
categoryItem = await this.categoryModel.findOneBy({ name: attr.name }); categoryItem = await this.categoryModel.findOneBy({ name: attr.name });
} else if (attr.title) {
// 尝试用 title 匹配 name 或 title
categoryItem = await this.categoryModel.findOne({
where: [
{ name: attr.title },
{ title: attr.title }
]
});
} }
continue; continue;
} }
let item: DictItem | null = null; let item: DictItem | null = null;
if (attr.id) { if (attr.id) {
// 中文注释:如果传入了 id直接查找字典项并使用不强制要求 dictName // 如果传入了 id,直接查找字典项并使用,不强制要求 dictName
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] }); item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`); if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
} else { } else {
// 中文注释:当未提供 id 时,需要 dictName 与 title/name 信息创建或获取字典项 // 当未提供 id 时,需要 dictName 与 title/name 信息创建或获取字典项
if (!attr?.dictName) throw new Error('属性项缺少字典名称'); if (!attr?.dictName) throw new Error('属性项缺少字典名称');
const titleOrName = attr.title || attr.name; const titleOrName = attr.title || attr.name;
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name'); if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
@ -378,7 +400,7 @@ export class ProductService {
resolvedAttributes.push(item); resolvedAttributes.push(item);
} }
// 检查完全相同属性组合是否已存在(中文注释:避免重复) // 检查完全相同属性组合是否已存在(避免重复)
const qb = this.productModel.createQueryBuilder('product'); const qb = this.productModel.createQueryBuilder('product');
resolvedAttributes.forEach((attr, index) => { resolvedAttributes.forEach((attr, index) => {
qb.innerJoin( qb.innerJoin(
@ -391,18 +413,21 @@ export class ProductService {
const isExist = await qb.getOne(); const isExist = await qb.getOne();
if (isExist) throw new Error('产品已存在'); if (isExist) throw new Error('产品已存在');
// 创建新产品实例(中文注释:绑定属性与基础字段) // 创建新产品实例(绑定属性与基础字段)
const product = new Product(); const product = new Product();
product.name = name;
product.description = description; // 使用 merge 填充基础字段,排除特殊处理字段
const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = createProductDTO;
this.productModel.merge(product, simpleFields);
product.attributes = resolvedAttributes; product.attributes = resolvedAttributes;
if (categoryItem) { if (categoryItem) {
product.category = categoryItem; product.category = categoryItem;
} }
// 条件判断(中文注释:设置商品类型,默认 single // 确保默认类型
product.type = (createProductDTO.type as any) || 'single'; if (!product.type) product.type = 'single';
// 生成或设置 SKU(中文注释:基于属性字典项的 name 生成) // 生成或设置 SKU(基于属性字典项的 name 生成)
if (sku) { if (sku) {
product.sku = sku; product.sku = sku;
} else { } else {
@ -418,15 +443,6 @@ export class ProductService {
}); });
} }
// 价格与促销价(中文注释:可选字段)
if (price !== undefined) {
product.price = Number(price);
}
const promotionPrice = (createProductDTO as any)?.promotionPrice;
if (promotionPrice !== undefined) {
product.promotionPrice = Number(promotionPrice);
}
return await this.productModel.save(product); return await this.productModel.save(product);
} }
@ -434,48 +450,42 @@ export class ProductService {
id: number, id: number,
updateProductDTO: UpdateProductDTO updateProductDTO: UpdateProductDTO
): Promise<Product> { ): Promise<Product> {
// 检查产品是否存在(包含属性关系) // 检查产品是否存在(包含属性关系)
const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict', 'category'] }); const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict', 'category'] });
if (!product) { if (!product) {
throw new Error(`产品 ID ${id} 不存在`); throw new Error(`产品 ID ${id} 不存在`);
} }
// 处理基础字段更新(若传入则更新) // 使用 merge 更新基础字段,排除特殊处理字段
if (updateProductDTO.name !== undefined) { const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = updateProductDTO;
product.name = updateProductDTO.name; this.productModel.merge(product, simpleFields);
}
if (updateProductDTO.description !== undefined) { // 处理分类更新
product.description = updateProductDTO.description;
}
if (updateProductDTO.categoryId !== undefined) { if (updateProductDTO.categoryId !== undefined) {
if (updateProductDTO.categoryId) { if (updateProductDTO.categoryId) {
const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } }); const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } });
if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`); if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`);
product.category = categoryItem; product.category = categoryItem;
} else { } else {
// 如果传了 0 或 null,可以清除分类(根据需求) // 如果传了 0 或 null,可以清除分类(根据需求)
// product.category = null; // product.category = null;
} }
} }
if (updateProductDTO.price !== undefined) {
product.price = Number(updateProductDTO.price); // 处理 SKU 更新
}
if ((updateProductDTO as any).promotionPrice !== undefined) {
product.promotionPrice = Number((updateProductDTO as any).promotionPrice);
}
if (updateProductDTO.sku !== undefined) { if (updateProductDTO.sku !== undefined) {
// 校验 SKU 唯一性(如变更) // 校验 SKU 唯一性(如变更)
const newSku = updateProductDTO.sku; const newSku = updateProductDTO.sku;
if (newSku && newSku !== product.sku) { if (newSku && newSku !== product.sku) {
const exist = await this.productModel.findOne({ where: { sku: newSku } }); const exist = await this.productModel.findOne({ where: { sku: newSku } });
if (exist) { if (exist) {
throw new Error('SKU 已存在请更换后重试'); throw new Error('SKU 已存在,请更换后重试');
} }
product.sku = newSku; product.sku = newSku;
} }
} }
// 处理属性更新(中文注释:若传入 attributes 则按字典名称替换对应项) // 处理属性更新(若传入 attributes 则按字典名称替换对应项)
if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) { if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) {
const nextAttributes: DictItem[] = [...(product.attributes || [])]; const nextAttributes: DictItem[] = [...(product.attributes || [])];
@ -485,7 +495,7 @@ export class ProductService {
}; };
for (const attr of updateProductDTO.attributes) { for (const attr of updateProductDTO.attributes) {
// 中文注释:如果属性是分类,特殊处理 // 如果属性是分类,特殊处理
if (attr.dictName === 'category') { if (attr.dictName === 'category') {
if (attr.id) { if (attr.id) {
const categoryItem = await this.categoryModel.findOneBy({ id: attr.id }); const categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
@ -496,17 +506,17 @@ export class ProductService {
let item: DictItem | null = null; let item: DictItem | null = null;
if (attr.id) { if (attr.id) {
// 中文注释:当提供 id 时直接查询字典项,不强制要求 dictName // 当提供 id 时直接查询字典项,不强制要求 dictName
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] }); item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`); if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
} else { } else {
// 中文注释:未提供 id 则需要 dictName 与 title/name 信息 // 未提供 id 则需要 dictName 与 title/name 信息
if (!attr?.dictName) throw new Error('属性项缺少字典名称'); if (!attr?.dictName) throw new Error('属性项缺少字典名称');
const titleOrName = attr.title || attr.name; const titleOrName = attr.title || attr.name;
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name'); if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name); item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
} }
// 中文注释:以传入的 dictName 或查询到的 item.dict.name 作为替换键 // 以传入的 dictName 或查询到的 item.dict.name 作为替换键
const dictKey = attr.dictName || item?.dict?.name; const dictKey = attr.dictName || item?.dict?.name;
if (!dictKey) throw new Error('无法确定字典名称用于替换属性'); if (!dictKey) throw new Error('无法确定字典名称用于替换属性');
replaceAttr(dictKey, item); replaceAttr(dictKey, item);
@ -515,7 +525,7 @@ export class ProductService {
product.attributes = nextAttributes; product.attributes = nextAttributes;
} }
// 条件判断(中文注释:更新商品类型,如传入) // 条件判断(更新商品类型,如传入)
if (updateProductDTO.type !== undefined) { if (updateProductDTO.type !== undefined) {
product.type = updateProductDTO.type as any; product.type = updateProductDTO.type as any;
} }
@ -525,14 +535,14 @@ export class ProductService {
return saved; return saved;
} }
// 中文注释:获取产品的库存组成列表(表关联版本) // 获取产品的库存组成列表(表关联版本)
async getProductComponents(productId: number): Promise<any[]> { async getProductComponents(productId: number): Promise<any[]> {
// 条件判断确保产品存在 // 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } }); const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`); if (!product) throw new Error(`产品 ID ${productId} 不存在`);
let components: ProductStockComponent[] = []; let components: ProductStockComponent[] = [];
// 条件判断(中文注释:单品 simple 不持久化组成,按 SKU 动态返回单条组成) // 条件判断(单品 simple 不持久化组成,按 SKU 动态返回单条组成)
if (product.type === 'single') { if (product.type === 'single') {
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
comp.productId = productId; comp.productId = productId;
@ -540,22 +550,22 @@ export class ProductService {
comp.quantity = 1; comp.quantity = 1;
components = [comp]; components = [comp];
} else { } else {
// 混装 bundle返回已保存的 SKU 组成 // 混装 bundle:返回已保存的 SKU 组成
components = await this.productStockComponentModel.find({ where: { productId } }); components = await this.productStockComponentModel.find({ where: { productId } });
} }
// 中文注释:获取所有组件的 SKU 列表 // 获取所有组件的 SKU 列表
const skus = components.map(c => c.sku); const skus = components.map(c => c.sku);
if (skus.length === 0) { if (skus.length === 0) {
return components; return components;
} }
// 中文注释:查询这些 SKU 的库存信息 // 查询这些 SKU 的库存信息
const stocks = await this.stockModel.find({ const stocks = await this.stockModel.find({
where: { sku: In(skus) }, where: { sku: In(skus) },
}); });
// 中文注释:获取所有相关的库存点 ID // 获取所有相关的库存点 ID
const stockPointIds = [...new Set(stocks.map(s => s.stockPointId))]; const stockPointIds = [...new Set(stocks.map(s => s.stockPointId))];
const stockPoints = await this.stockPointModel.find({ where: { id: In(stockPointIds) } }); const stockPoints = await this.stockPointModel.find({ where: { id: In(stockPointIds) } });
const stockPointMap = stockPoints.reduce((map, sp) => { const stockPointMap = stockPoints.reduce((map, sp) => {
@ -563,7 +573,7 @@ export class ProductService {
return map; return map;
}, {}); }, {});
// 中文注释:将库存信息按 SKU 分组 // 将库存信息按 SKU 分组
const stockMap = stocks.reduce((map, stock) => { const stockMap = stocks.reduce((map, stock) => {
if (!map[stock.sku]) { if (!map[stock.sku]) {
map[stock.sku] = []; map[stock.sku] = [];
@ -578,7 +588,7 @@ export class ProductService {
return map; return map;
}, {}); }, {});
// 中文注释:将库存信息附加到组件上 // 将库存信息附加到组件上
const componentsWithStock = components.map(comp => { const componentsWithStock = components.map(comp => {
return { return {
...comp, ...comp,
@ -589,15 +599,15 @@ export class ProductService {
return componentsWithStock; return componentsWithStock;
} }
// 中文注释:设置产品的库存组成(覆盖式,表关联版本) // 设置产品的库存组成(覆盖式,表关联版本)
async setProductComponents( async setProductComponents(
productId: number, productId: number,
items: { sku: string; quantity: number }[] items: { sku: string; quantity: number }[]
): Promise<ProductStockComponent[]> { ): Promise<ProductStockComponent[]> {
// 条件判断确保产品存在 // 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } }); const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`); if (!product) throw new Error(`产品 ID ${productId} 不存在`);
// 条件判断(中文注释:单品 simple 不允许手动设置组成) // 条件判断(单品 simple 不允许手动设置组成)
if (product.type === 'single') { if (product.type === 'single') {
throw new Error('单品无需设置组成'); throw new Error('单品无需设置组成');
} }
@ -612,7 +622,7 @@ export class ProductService {
// 插入新的组成 // 插入新的组成
const created: ProductStockComponent[] = []; const created: ProductStockComponent[] = [];
for (const i of validItems) { for (const i of validItems) {
// 中文注释:校验 SKU 格式,允许不存在库存但必须非空 // 校验 SKU 格式,允许不存在库存但必须非空
if (!i.sku || i.sku.trim().length === 0) { if (!i.sku || i.sku.trim().length === 0) {
throw new Error('SKU 不能为空'); throw new Error('SKU 不能为空');
} }
@ -625,13 +635,13 @@ export class ProductService {
return created; return created;
} }
// 中文注释:根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1 // 根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1)
async autoBindComponentsBySku(productId: number): Promise<ProductStockComponent[]> { async autoBindComponentsBySku(productId: number): Promise<ProductStockComponent[]> {
// 条件判断确保产品存在 // 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } }); const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`); if (!product) throw new Error(`产品 ID ${productId} 不存在`);
// 中文注释:按 SKU 自动绑定 // 按 SKU 自动绑定
// 条件判断simple 类型不持久化组成,直接返回单条基于 SKU 的组成 // 条件判断:simple 类型不持久化组成,直接返回单条基于 SKU 的组成
if (product.type === 'single') { if (product.type === 'single') {
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
comp.productId = productId; comp.productId = productId;
@ -639,7 +649,7 @@ export class ProductService {
comp.quantity = 1; // 默认数量 1 comp.quantity = 1; // 默认数量 1
return [comp]; return [comp];
} }
// bundle 类型若不存在则持久化一条基于 SKU 的组成 // bundle 类型:若不存在则持久化一条基于 SKU 的组成
const exist = await this.productStockComponentModel.findOne({ where: { productId, sku: product.sku } }); const exist = await this.productStockComponentModel.findOne({ where: { productId, sku: product.sku } });
if (!exist) { if (!exist) {
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
@ -651,7 +661,7 @@ export class ProductService {
return await this.getProductComponents(productId); return await this.getProductComponents(productId);
} }
// 重复定义的 getProductList 已合并到前面的实现(中文注释:移除重复) // 重复定义的 getProductList 已合并到前面的实现(移除重复)
async updatenameCn(id: number, nameCn: string): Promise<Product> { async updatenameCn(id: number, nameCn: string): Promise<Product> {
// 确认产品是否存在 // 确认产品是否存在
@ -681,7 +691,7 @@ export class ProductService {
}) })
.getOne(); .getOne();
if (wpProduct) { if (wpProduct) {
throw new Error('无法删除请先删除关联的WP产品'); throw new Error('无法删除,请先删除关联的WP产品');
} }
const variation = await this.variationModel const variation = await this.variationModel
@ -693,7 +703,7 @@ export class ProductService {
if (variation) { if (variation) {
console.log(variation); console.log(variation);
throw new Error('无法删除请先删除关联的WP变体'); throw new Error('无法删除,请先删除关联的WP变体');
} }
// 删除产品 // 删除产品
@ -739,7 +749,7 @@ export class ProductService {
where: { name: 'brand' }, where: { name: 'brand' },
}); });
// 如果字典不存在则返回空 // 如果字典不存在,则返回空
if (!brandDict) { if (!brandDict) {
return { return {
items: [], items: [],
@ -764,7 +774,7 @@ export class ProductService {
where: { name: 'brand' }, where: { name: 'brand' },
}); });
// 如果字典不存在则返回空数组 // 如果字典不存在,则返回空数组
if (!brandDict) { if (!brandDict) {
return []; return [];
} }
@ -781,7 +791,7 @@ export class ProductService {
where: { name: 'brand' }, where: { name: 'brand' },
}); });
// 如果字典不存在则抛出错误 // 如果字典不存在,则抛出错误
if (!brandDict) { if (!brandDict) {
throw new Error('品牌字典不存在'); throw new Error('品牌字典不存在');
} }
@ -894,9 +904,9 @@ export class ProductService {
pagination: PaginationParams, pagination: PaginationParams,
title?: string title?: string
): Promise<SizePaginatedResponse> { ): Promise<SizePaginatedResponse> {
// 查找 'size' 字典(中文注释:用于尺寸) // 查找 'size' 字典(用于尺寸)
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } }); const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
// 条件判断(中文注释:如果字典不存在则返回空分页) // 条件判断(如果字典不存在则返回空分页)
if (!sizeDict) { if (!sizeDict) {
return { return {
items: [], items: [],
@ -904,19 +914,19 @@ export class ProductService {
...pagination, ...pagination,
} as any; } as any;
} }
// 构建 where 条件(中文注释:按标题模糊搜索) // 构建 where 条件(按标题模糊搜索)
const where: any = { dict: { id: sizeDict.id } }; const where: any = { dict: { id: sizeDict.id } };
if (title) { if (title) {
where.title = Like(`%${title}%`); where.title = Like(`%${title}%`);
} }
// 分页查询(中文注释:复用通用分页工具) // 分页查询(复用通用分页工具)
return await paginate(this.dictItemModel, { pagination, where }); return await paginate(this.dictItemModel, { pagination, where });
} }
async getSizeAll(): Promise<SizePaginatedResponse> { async getSizeAll(): Promise<SizePaginatedResponse> {
// 查找 'size' 字典(中文注释:获取所有尺寸项) // 查找 'size' 字典(获取所有尺寸项)
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } }); const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
// 条件判断(中文注释:如果字典不存在返回空数组) // 条件判断(如果字典不存在返回空数组)
if (!sizeDict) { if (!sizeDict) {
return [] as any; return [] as any;
} }
@ -925,13 +935,13 @@ export class ProductService {
async createSize(createSizeDTO: any): Promise<DictItem> { async createSize(createSizeDTO: any): Promise<DictItem> {
const { title, name } = createSizeDTO; const { title, name } = createSizeDTO;
// 获取 size 字典(中文注释:用于挂载尺寸项) // 获取 size 字典(用于挂载尺寸项)
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } }); const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
// 条件判断(中文注释:尺寸字典不存在则抛错) // 条件判断(尺寸字典不存在则抛错)
if (!sizeDict) { if (!sizeDict) {
throw new Error('尺寸字典不存在'); throw new Error('尺寸字典不存在');
} }
// 创建字典项(中文注释:保存尺寸名称与唯一标识) // 创建字典项(保存尺寸名称与唯一标识)
const size = new DictItem(); const size = new DictItem();
size.title = title; size.title = title;
size.name = name; size.name = name;
@ -940,26 +950,26 @@ export class ProductService {
} }
async updateSize(id: number, updateSize: any) { async updateSize(id: number, updateSize: any) {
// 先查询(中文注释:确保尺寸项存在) // 先查询(确保尺寸项存在)
const size = await this.dictItemModel.findOneBy({ id }); const size = await this.dictItemModel.findOneBy({ id });
// 条件判断(中文注释:不存在则报错) // 条件判断(不存在则报错)
if (!size) { if (!size) {
throw new Error(`尺寸 ID ${id} 不存在`); throw new Error(`尺寸 ID ${id} 不存在`);
} }
// 更新(中文注释:写入变更字段) // 更新(写入变更字段)
await this.dictItemModel.update(id, updateSize); await this.dictItemModel.update(id, updateSize);
// 返回最新(中文注释:再次查询返回) // 返回最新(再次查询返回)
return await this.dictItemModel.findOneBy({ id }); return await this.dictItemModel.findOneBy({ id });
} }
async deleteSize(id: number): Promise<boolean> { async deleteSize(id: number): Promise<boolean> {
// 先查询(中文注释:确保尺寸项存在) // 先查询(确保尺寸项存在)
const size = await this.dictItemModel.findOneBy({ id }); const size = await this.dictItemModel.findOneBy({ id });
// 条件判断(中文注释:不存在则报错) // 条件判断(不存在则报错)
if (!size) { if (!size) {
throw new Error(`尺寸 ID ${id} 不存在`); throw new Error(`尺寸 ID ${id} 不存在`);
} }
// 删除(中文注释:执行删除并返回受影响行数是否>0 // 删除(执行删除并返回受影响行数是否>0)
const result = await this.dictItemModel.delete(id); const result = await this.dictItemModel.delete(id);
return result.affected > 0; return result.affected > 0;
} }
@ -1025,7 +1035,7 @@ export class ProductService {
return await this.dictItemModel.save(strength); return await this.dictItemModel.save(strength);
} }
// 通用属性分页获取指定字典的字典项 // 通用属性:分页获取指定字典的字典项
async getAttributeList( async getAttributeList(
dictName: string, dictName: string,
pagination: PaginationParams, pagination: PaginationParams,
@ -1045,7 +1055,7 @@ export class ProductService {
return { items, total, ...pagination } as any; return { items, total, ...pagination } as any;
} }
// 通用属性获取指定字典的全部字典项 // 通用属性:获取指定字典的全部字典项
async getAttributeAll(dictName: string): Promise<DictItem[]> { async getAttributeAll(dictName: string): Promise<DictItem[]> {
const dict = await this.dictModel.findOne({ where: { name: dictName } }); const dict = await this.dictModel.findOne({ where: { name: dictName } });
if (!dict) return []; if (!dict) return [];
@ -1056,7 +1066,7 @@ export class ProductService {
}); });
} }
// 通用属性创建字典项 // 通用属性:创建字典项
async createAttribute( async createAttribute(
dictName: string, dictName: string,
payload: { title: string; name: string } payload: { title: string; name: string }
@ -1075,7 +1085,7 @@ export class ProductService {
return await this.dictItemModel.save(item); return await this.dictItemModel.save(item);
} }
// 通用属性更新字典项 // 通用属性:更新字典项
async updateAttribute( async updateAttribute(
id: number, id: number,
payload: { title?: string; name?: string } payload: { title?: string; name?: string }
@ -1087,10 +1097,10 @@ export class ProductService {
return await this.dictItemModel.save(item); return await this.dictItemModel.save(item);
} }
// 通用属性:删除字典项(若存在产品关联则禁止删除) // 通用属性:删除字典项(若存在产品关联则禁止删除)
async deleteAttribute(id: number): Promise<void> { async deleteAttribute(id: number): Promise<void> {
const hasProducts = await this.hasProductsInAttribute(id); const hasProducts = await this.hasProductsInAttribute(id);
if (hasProducts) throw new Error('当前字典项存在关联产品无法删除'); if (hasProducts) throw new Error('当前字典项存在关联产品,无法删除');
await this.dictItemModel.delete({ id }); await this.dictItemModel.delete({ id });
} }
@ -1126,7 +1136,7 @@ export class ProductService {
throw new Error(`以下 SKU 已存在: ${existingSkus.join(', ')}`); throw new Error(`以下 SKU 已存在: ${existingSkus.join(', ')}`);
} }
// 遍历检查产品 ID 是否存在并更新 sku // 遍历检查产品 ID 是否存在,并更新 sku
for (const { productId, sku } of skus) { for (const { productId, sku } of skus) {
const product = await this.productModel.findOne({ const product = await this.productModel.findOne({
where: { id: productId }, where: { id: productId },
@ -1142,31 +1152,131 @@ export class ProductService {
return `成功更新 ${skus.length} 个 sku`; return `成功更新 ${skus.length} 个 sku`;
} }
// 中文注释:导出所有产品为 CSV 文本 // 将单条 CSV 记录转换为数据对象
async exportProductsCSV(): Promise<string> { transformCsvRecordToData(rec: any): CreateProductDTO & { sku: string } | null {
// 查询所有产品及其属性(中文注释:包含字典关系) // 必须包含 sku
const products = await this.productModel.find({ const sku: string = (rec.sku || '').trim();
relations: ['attributes', 'attributes.dict'], if (!sku) {
order: { id: 'ASC' }, return null;
}); }
// 定义 CSV 表头(中文注释:与导入字段一致) // 辅助函数:处理空字符串为 undefined
const headers = [ const val = (v: any) => {
'sku', if (v === undefined || v === null) return undefined;
'name', const s = String(v).trim();
'nameCn', return s === '' ? undefined : s;
'price', };
'promotionPrice',
'type',
'stock',
'brand',
'flavor',
'strength',
'size',
'description',
];
// 中文注释CSV 字段转义,处理逗号与双引号 // 辅助函数:处理数字
const num = (v: any) => {
const s = val(v);
return s ? Number(s) : undefined;
};
// 解析属性字段(分号分隔多值)
const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []);
// 将属性解析为 DTO 输入
const attributes: any[] = [];
// 处理动态属性字段 (attribute_*)
for (const key of Object.keys(rec)) {
if (key.startsWith('attribute_')) {
const dictName = key.replace('attribute_', '');
if (dictName) {
const list = parseList(rec[key]);
for (const item of list) attributes.push({ dictName, title: item });
}
}
}
// 解析组件信息 (component_*)
const componentsMap = new Map<string, { sku?: string; quantity?: number }>();
for (const key of Object.keys(rec)) {
const skuMatch = key.match(/^component_(\d+)_sku$/);
if (skuMatch) {
const idx = skuMatch[1];
if (!componentsMap.has(idx)) componentsMap.set(idx, {});
componentsMap.get(idx)!.sku = rec[key];
}
const qtyMatch = key.match(/^component_(\d+)_quantity$/);
if (qtyMatch) {
const idx = qtyMatch[1];
if (!componentsMap.has(idx)) componentsMap.set(idx, {});
componentsMap.get(idx)!.quantity = Number(rec[key]);
}
}
const components = Array.from(componentsMap.values())
.filter(c => c.sku && c.quantity)
.map(c => ({ sku: c.sku!, quantity: c.quantity! }));
return {
sku,
name: val(rec.name),
nameCn: val(rec.nameCn),
description: val(rec.description),
price: num(rec.price),
promotionPrice: num(rec.promotionPrice),
type: val(rec.type),
attributes: attributes.length > 0 ? attributes : undefined,
components: components.length > 0 ? components : undefined,
} as any;
}
// 准备创建产品的 DTO, 处理类型转换和默认值
prepareCreateProductDTO(data: any): CreateProductDTO {
const dto = new CreateProductDTO();
// 基础字段赋值
dto.name = data.name;
dto.nameCn = data.nameCn;
dto.description = data.description;
dto.sku = data.sku;
// 数值类型转换
if (data.price !== undefined) dto.price = Number(data.price);
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId);
// 默认值和特殊处理
dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
// 如果有组件信息,透传
dto.type = data.type || data.components?.length? 'bundle':'single'
if (data.components) dto.components = data.components;
return dto;
}
// 准备更新产品的 DTO, 处理类型转换
prepareUpdateProductDTO(data: any): UpdateProductDTO {
const dto = new UpdateProductDTO();
if (data.name !== undefined) dto.name = data.name;
if (data.nameCn !== undefined) dto.nameCn = data.nameCn;
if (data.description !== undefined) dto.description = data.description;
if (data.sku !== undefined) dto.sku = data.sku;
if (data.price !== undefined) dto.price = Number(data.price);
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId);
if (data.type !== undefined) dto.type = data.type;
if (data.attributes !== undefined) dto.attributes = data.attributes;
return dto;
}
// 将单个产品转换为 CSV 行数组
transformProductToCsvRow(
p: Product,
sortedDictNames: string[],
maxComponentCount: number
): string[] {
// CSV 字段转义,处理逗号与双引号
const esc = (v: any) => { const esc = (v: any) => {
const s = v === undefined || v === null ? '' : String(v); const s = v === undefined || v === null ? '' : String(v);
const needsQuote = /[",\n]/.test(s); const needsQuote = /[",\n]/.test(s);
@ -1174,7 +1284,7 @@ export class ProductService {
return needsQuote ? `"${escaped}"` : escaped; return needsQuote ? `"${escaped}"` : escaped;
}; };
// 中文注释:将属性列表转为字典名到显示值的映射 // 将属性列表转为字典名到显示值的映射
const pickAttr = (prod: Product, key: string) => { const pickAttr = (prod: Product, key: string) => {
const list = (prod.attributes || []).filter(a => a?.dict?.name === key); const list = (prod.attributes || []).filter(a => a?.dict?.name === key);
if (list.length === 0) return ''; if (list.length === 0) return '';
@ -1182,33 +1292,107 @@ export class ProductService {
return list.map(a => a.title || a.name).join(';'); return list.map(a => a.title || a.name).join(';');
}; };
const rows: string[] = []; // 基础数据
rows.push(headers.join(',')); const rowData = [
esc(p.sku),
esc(p.name),
esc(p.nameCn),
esc(p.price),
esc(p.promotionPrice),
esc(p.type),
esc(p.description),
];
// 属性数据
for (const dictName of sortedDictNames) {
rowData.push(esc(pickAttr(p, dictName)));
}
// 组件数据
const components = p.components || [];
for (let i = 0; i < maxComponentCount; i++) {
const comp = components[i];
if (comp) {
rowData.push(esc(comp.sku));
rowData.push(esc(comp.quantity));
} else {
rowData.push('');
rowData.push('');
}
}
return rowData;
}
// 导出所有产品为 CSV 文本
async exportProductsCSV(): Promise<string> {
// 查询所有产品及其属性(包含字典关系)和组成
const products = await this.productModel.find({
relations: ['attributes', 'attributes.dict', 'components'],
order: { id: 'ASC' },
});
// 1. 收集所有动态属性的 dictName
const dictNames = new Set<string>();
// 2. 收集最大的组件数量
let maxComponentCount = 0;
for (const p of products) { for (const p of products) {
// 中文注释:逐行输出产品数据 if (p.attributes) {
const row = [ for (const attr of p.attributes) {
esc(p.sku), if (attr.dict && attr.dict.name) {
esc(p.name), dictNames.add(attr.dict.name);
esc(p.nameCn), }
esc(p.price), }
esc(p.promotionPrice), }
esc(p.type), if (p.components) {
esc(p.stock), if (p.components.length > maxComponentCount) {
esc(pickAttr(p, 'brand')), maxComponentCount = p.components.length;
esc(pickAttr(p, 'flavor')), }
esc(pickAttr(p, 'strength')), }
esc(pickAttr(p, 'size')), }
esc(p.description),
].join(','); const sortedDictNames = Array.from(dictNames).sort();
rows.push(row);
// 定义 CSV 表头(与导入字段一致)
const baseHeaders = [
'sku',
'name',
'nameCn',
'price',
'promotionPrice',
'type',
'description',
];
// 动态属性表头
const attributeHeaders = sortedDictNames.map(name => `attribute_${name}`);
// 动态组件表头
const componentHeaders = [];
for (let i = 1; i <= maxComponentCount; i++) {
componentHeaders.push(`component_${i}_sku`);
componentHeaders.push(`component_${i}_quantity`);
}
const allHeaders = [...baseHeaders, ...attributeHeaders, ...componentHeaders];
const rows: string[] = [];
rows.push(allHeaders.join(','));
for (const p of products) {
const rowData = this.transformProductToCsvRow(p, sortedDictNames, maxComponentCount);
rows.push(rowData.join(','));
} }
return rows.join('\n'); return rows.join('\n');
} }
// 中文注释:从 CSV 导入产品;存在则更新,不存在则创建 // 从 CSV 导入产品;存在则更新,不存在则创建
async importProductsCSV(buffer: Buffer): Promise<{ created: number; updated: number; errors: string[] }> { async importProductsCSV(buffer: Buffer): Promise<{ created: number; updated: number; errors: string[] }> {
// 解析 CSV中文注释使用 csv-parse/sync 按表头解析) // 解析 CSV(使用 csv-parse/sync 按表头解析)
const { parse } = await import('csv-parse/sync'); const { parse } = await import('csv-parse/sync');
let records: any[] = []; let records: any[] = [];
try { try {
@ -1216,90 +1400,56 @@ export class ProductService {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true,
trim: true, trim: true,
bom: true,
}); });
console.log('Parsed records count:', records.length);
if (records.length > 0) {
console.log('First record keys:', Object.keys(records[0]));
}
} catch (e: any) { } catch (e: any) {
return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] };
} }
let created = 0; let created = 0;
let updated = 0; let updated = 0;
const errors: string[] = []; const errors: string[] = [];
// 中文注释:逐条处理记录 // 逐条处理记录
for (const rec of records) { for (const rec of records) {
try { try {
// 条件判断:必须包含 sku const data = this.transformCsvRecordToData(rec);
const sku: string = (rec.sku || '').trim(); if (!data) {
if (!sku) {
// 缺少 SKU 直接跳过
errors.push('缺少 SKU 的记录已跳过'); errors.push('缺少 SKU 的记录已跳过');
continue; continue;
} }
const { sku, components } = data;
let currentProductId: number;
let currentProductType: string = data.type || 'single';
// 查找现有产品 // 查找现有产品
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] }); const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
// 中文注释:准备基础字段
const base = {
name: rec.name || '',
nameCn: rec.nameCn || '',
description: rec.description || '',
price: rec.price ? Number(rec.price) : undefined,
promotionPrice: rec.promotionPrice ? Number(rec.promotionPrice) : undefined,
type: rec.type || '',
stock: rec.stock ? Number(rec.stock) : undefined,
sku,
} as any;
// 中文注释:解析属性字段(分号分隔多值)
const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []);
const brands = parseList(rec.brand);
const flavors = parseList(rec.flavor);
const strengths = parseList(rec.strength);
const sizes = parseList(rec.size);
// 中文注释:将属性解析为 DTO 输入
const attrDTOs: { dictName: string; title: string }[] = [];
for (const b of brands) attrDTOs.push({ dictName: 'brand', title: b });
for (const f of flavors) attrDTOs.push({ dictName: 'flavor', title: f });
for (const s of strengths) attrDTOs.push({ dictName: 'strength', title: s });
for (const z of sizes) attrDTOs.push({ dictName: 'size', title: z });
if (!exist) { if (!exist) {
// 中文注释:创建新产品 // 创建新产品
const dto = { const createDTO = this.prepareCreateProductDTO(data);
name: base.name, const createdProduct = await this.createProduct(createDTO);
description: base.description, currentProductId = createdProduct.id;
price: base.price, currentProductType = createdProduct.type;
sku: base.sku,
attributes: attrDTOs,
} as any;
const createdProduct = await this.createProduct(dto);
// 条件判断:更新可选字段
const patch: any = {};
if (base.nameCn) patch.nameCn = base.nameCn;
if (base.promotionPrice !== undefined) patch.promotionPrice = base.promotionPrice;
if (base.type) patch.type = base.type;
if (base.stock !== undefined) patch.stock = base.stock;
if (Object.keys(patch).length > 0) await this.productModel.update(createdProduct.id, patch);
created += 1; created += 1;
} else { } else {
// 中文注释:更新产品 // 更新产品
const updateDTO: any = { const updateDTO = this.prepareUpdateProductDTO(data);
name: base.name || exist.name,
description: base.description || exist.description,
price: base.price !== undefined ? base.price : exist.price,
sku: base.sku,
attributes: attrDTOs,
};
// 条件判断:附加可选字段
if (base.nameCn) updateDTO.nameCn = base.nameCn;
if (base.promotionPrice !== undefined) updateDTO.promotionPrice = base.promotionPrice;
if (base.type) updateDTO.type = base.type;
if (base.stock !== undefined) updateDTO.stock = base.stock;
await this.updateProduct(exist.id, updateDTO); await this.updateProduct(exist.id, updateDTO);
currentProductId = exist.id;
currentProductType = updateDTO.type || exist.type;
updated += 1; updated += 1;
} }
// 4. 保存组件信息
if (currentProductType !== 'single' && components && components.length > 0) {
await this.setProductComponents(currentProductId, components);
}
} catch (e: any) { } catch (e: any) {
errors.push(e?.message || String(e)); errors.push(e?.message || String(e));
} }

View File

@ -16,7 +16,7 @@ export class SiteService {
areaModel: Repository<Area>; areaModel: Repository<Area>;
async syncFromConfig(sites: WpSite[] = []) { async syncFromConfig(sites: WpSite[] = []) {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
for (const siteConfig of sites) { for (const siteConfig of sites) {
// 按站点名称查询是否已存在记录 // 按站点名称查询是否已存在记录
const exist = await this.siteModel.findOne({ const exist = await this.siteModel.findOne({
@ -30,7 +30,7 @@ export class SiteService {
consumerSecret: (siteConfig as any).consumerSecret, consumerSecret: (siteConfig as any).consumerSecret,
type: 'woocommerce', type: 'woocommerce',
}; };
// 存在则更新不存在则插入新记录 // 存在则更新,不存在则插入新记录
if (exist) { if (exist) {
await this.siteModel.update({ id: exist.id }, payload); await this.siteModel.update({ id: exist.id }, payload);
} else { } else {
@ -45,14 +45,14 @@ export class SiteService {
const newSite = new Site(); const newSite = new Site();
Object.assign(newSite, restData); Object.assign(newSite, restData);
// 如果传入了区域代码则查询并关联 Area 实体 // 如果传入了区域代码,则查询并关联 Area 实体
if (areaCodes && areaCodes.length > 0) { if (areaCodes && areaCodes.length > 0) {
const areas = await this.areaModel.findBy({ const areas = await this.areaModel.findBy({
code: In(areaCodes), code: In(areaCodes),
}); });
newSite.areas = areas; newSite.areas = areas;
} else { } else {
// 如果没有传入区域,则关联一个空数组,代表“全局” // 如果没有传入区域,则关联一个空数组,代表"全局"
newSite.areas = []; newSite.areas = [];
} }
@ -65,12 +65,12 @@ export class SiteService {
// 从 DTO 中分离出区域代码和其他站点数据 // 从 DTO 中分离出区域代码和其他站点数据
const { areas: areaCodes, ...restData } = data; const { areas: areaCodes, ...restData } = data;
// 首先根据 ID 查找要更新的站点实体 // 首先,根据 ID 查找要更新的站点实体
const siteToUpdate = await this.siteModel.findOne({ const siteToUpdate = await this.siteModel.findOne({
where: { id: Number(id) }, where: { id: Number(id) },
}); });
if (!siteToUpdate) { if (!siteToUpdate) {
// 如果找不到站点则操作失败 // 如果找不到站点,则操作失败
return false; return false;
} }
@ -86,16 +86,16 @@ export class SiteService {
} as any; } as any;
Object.assign(siteToUpdate, payload); Object.assign(siteToUpdate, payload);
// 如果 DTO 中传入了 areas 字段(即使是空数组),也要更新关联关系 // 如果 DTO 中传入了 areas 字段(即使是空数组),也要更新关联关系
if (areaCodes !== undefined) { if (areaCodes !== undefined) {
if (areaCodes.length > 0) { if (areaCodes.length > 0) {
// 如果区域代码数组不为空则查找并更新关联 // 如果区域代码数组不为空,则查找并更新关联
const areas = await this.areaModel.findBy({ const areas = await this.areaModel.findBy({
code: In(areaCodes), code: In(areaCodes),
}); });
siteToUpdate.areas = areas; siteToUpdate.areas = areas;
} else { } else {
// 如果传入空数组,则清空所有关联,代表“全局” // 如果传入空数组,则清空所有关联,代表"全局"
siteToUpdate.areas = []; siteToUpdate.areas = [];
} }
} }
@ -106,7 +106,7 @@ export class SiteService {
} }
async get(id: string | number, includeSecret = false) { async get(id: string | number, includeSecret = false) {
// 根据主键获取站点并使用 relations 加载关联的 areas // 根据主键获取站点,并使用 relations 加载关联的 areas
const site = await this.siteModel.findOne({ const site = await this.siteModel.findOne({
where: { id: Number(id) }, where: { id: Number(id) },
relations: ['areas'], relations: ['areas'],
@ -114,11 +114,11 @@ export class SiteService {
if (!site) { if (!site) {
return null; return null;
} }
// 如果需要包含密钥则直接返回 // 如果需要包含密钥,则直接返回
if (includeSecret) { if (includeSecret) {
return site; return site;
} }
// 默认不返回密钥进行字段脱敏 // 默认不返回密钥,进行字段脱敏
const { consumerKey, consumerSecret, ...rest } = site; const { consumerKey, consumerSecret, ...rest } = site;
return rest; return rest;
} }
@ -133,7 +133,7 @@ export class SiteService {
}, },
includeSecret = false includeSecret = false
) { ) {
// 分页查询站点列表,支持关键字、禁用状态与 ID 列表过滤 // 分页查询站点列表,支持关键字,禁用状态与 ID 列表过滤
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param ||
{}) as any; {}) as any;
const where: any = {}; const where: any = {};
@ -141,12 +141,12 @@ export class SiteService {
if (keyword) { if (keyword) {
where.name = Like(`%${keyword}%`); where.name = Like(`%${keyword}%`);
} }
// 按禁用状态过滤(布尔转数值) // 按禁用状态过滤(布尔转数值)
if (typeof isDisabled === 'boolean') { if (typeof isDisabled === 'boolean') {
where.isDisabled = isDisabled ? 1 : 0; where.isDisabled = isDisabled ? 1 : 0;
} }
if (ids) { if (ids) {
// 解析逗号分隔的 ID 字符串为数字数组并过滤非法值 // 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
const numIds = String(ids) const numIds = String(ids)
.split(',') .split(',')
.filter(Boolean) .filter(Boolean)
@ -156,7 +156,7 @@ export class SiteService {
where.id = In(numIds); where.id = In(numIds);
} }
} }
// 进行分页查询并使用 relations 加载关联的 areas // 进行分页查询,并使用 relations 加载关联的 areas
const [items, total] = await this.siteModel.findAndCount({ const [items, total] = await this.siteModel.findAndCount({
where, where,
skip: (current - 1) * pageSize, skip: (current - 1) * pageSize,
@ -174,7 +174,7 @@ export class SiteService {
} }
async disable(id: string | number, disabled: boolean) { async disable(id: string | number, disabled: boolean) {
// 设置站点禁用状态true -> 1, false -> 0 // 设置站点禁用状态(true -> 1, false -> 0)
await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled }); await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled });
return true; return true;
} }

View File

@ -1011,7 +1011,7 @@ export class StatisticsService {
GROUP BY customer_email GROUP BY customer_email
), ),
-- -- "新客户""老客户"
labeled_users AS ( labeled_users AS (
SELECT SELECT
m.customer_email, m.customer_email,
@ -1056,7 +1056,7 @@ export class StatisticsService {
GROUP BY current_month GROUP BY current_month
) )
-- -- 最终结果:每月新客户,,
SELECT SELECT
m.order_month, m.order_month,
m.new_user_count, m.new_user_count,

View File

@ -139,7 +139,7 @@ export class StockService {
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id }); const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`); if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
if (purchaseOrder.status === 'received') if (purchaseOrder.status === 'received')
throw new Error(`采购订单 ID ${id} 已到达无法修改`); throw new Error(`采购订单 ID ${id} 已到达,无法修改`);
const { stockPointId, expectedArrivalTime, status, items, note } = data; const { stockPointId, expectedArrivalTime, status, items, note } = data;
purchaseOrder.stockPointId = stockPointId; purchaseOrder.stockPointId = stockPointId;
purchaseOrder.expectedArrivalTime = expectedArrivalTime; purchaseOrder.expectedArrivalTime = expectedArrivalTime;
@ -208,7 +208,7 @@ export class StockService {
); );
} }
// 中文注释:检查指定 SKU 是否在任一仓库有库存(数量大于 0 // 检查指定 SKU 是否在任一仓库有库存(数量大于 0)
async hasStockBySku(sku: string): Promise<boolean> { async hasStockBySku(sku: string): Promise<boolean> {
const count = await this.stockModel const count = await this.stockModel
.createQueryBuilder('stock') .createQueryBuilder('stock')
@ -222,7 +222,7 @@ export class StockService {
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id }); const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`); if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
if (purchaseOrder.status === 'received') if (purchaseOrder.status === 'received')
throw new Error(`采购订单 ID ${id} 已到达无法删除`); throw new Error(`采购订单 ID ${id} 已到达,无法删除`);
await this.purchaseOrderItemModel.delete({ purchaseOrderId: id }); await this.purchaseOrderItemModel.delete({ purchaseOrderId: id });
await this.purchaseOrderModel.delete({ id }); await this.purchaseOrderModel.delete({ id });
} }
@ -231,7 +231,7 @@ export class StockService {
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id }); const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`); if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
if (purchaseOrder.status === 'received') if (purchaseOrder.status === 'received')
throw new Error(`采购订单 ID ${id} 已到达不要重复操作`); throw new Error(`采购订单 ID ${id} 已到达,不要重复操作`);
const items = await this.purchaseOrderItemModel.find({ const items = await this.purchaseOrderItemModel.find({
where: { purchaseOrderId: id }, where: { purchaseOrderId: id },
}); });
@ -403,7 +403,7 @@ export class StockService {
sku, sku,
}); });
if (!stock) { if (!stock) {
// 如果库存不存在则直接新增 // 如果库存不存在,则直接新增
const newStock = this.stockModel.create({ const newStock = this.stockModel.create({
stockPointId, stockPointId,
sku, sku,
@ -415,7 +415,7 @@ export class StockService {
stock.quantity += stock.quantity +=
operationType === 'in' ? quantityChange : -quantityChange; operationType === 'in' ? quantityChange : -quantityChange;
// if (stock.quantity < 0) { // if (stock.quantity < 0) {
// throw new Error('库存不足无法完成操作'); // throw new Error('库存不足,无法完成操作');
// } // }
await this.stockModel.save(stock); await this.stockModel.save(stock);
} }
@ -571,7 +571,7 @@ export class StockService {
async cancelTransfer(id: number, userId: number) { async cancelTransfer(id: number, userId: number) {
const transfer = await this.transferModel.findOneBy({ id }); const transfer = await this.transferModel.findOneBy({ id });
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`); if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
if (transfer.isArrived) throw new Error(`调拨 ID ${id} 已到达无法取消`); if (transfer.isArrived) throw new Error(`调拨 ID ${id} 已到达,无法取消`);
const items = await this.transferItemModel.find({ const items = await this.transferItemModel.find({
where: { transferId: id }, where: { transferId: id },
}); });
@ -594,7 +594,7 @@ export class StockService {
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`); if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`); if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`);
if (transfer.isArrived) if (transfer.isArrived)
throw new Error(`调拨 ID ${id} 已到达不要重复操作`); throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
const items = await this.transferItemModel.find({ const items = await this.transferItemModel.find({
where: { transferId: id }, where: { transferId: id },
}); });
@ -617,7 +617,7 @@ export class StockService {
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`); if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`); if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`);
if (transfer.isArrived) if (transfer.isArrived)
throw new Error(`调拨 ID ${id} 已到达不要重复操作`); throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
transfer.isLost = true; transfer.isLost = true;
await this.transferModel.save(transfer); await this.transferModel.save(transfer);
} }

View File

@ -28,8 +28,8 @@ export class SubscriptionService {
/** /**
* *
* - externalSubscriptionId * - , externalSubscriptionId
* - * - ,
*/ */
async syncSingleSubscription(siteId: number, sub: any) { async syncSingleSubscription(siteId: number, sub: any) {
const { line_items, ...raw } = sub; const { line_items, ...raw } = sub;
@ -54,7 +54,7 @@ export class SubscriptionService {
} }
/** /**
* * (,,)
*/ */
async getSubscriptionList({ async getSubscriptionList({
current = 1, current = 1,

View File

@ -64,7 +64,7 @@ export class TemplateService {
): Promise<Template> { ): Promise<Template> {
// 首先根据 ID 查找模板 // 首先根据 ID 查找模板
const template = await this.templateModel.findOneBy({ id }); const template = await this.templateModel.findOneBy({ id });
// 如果模板不存在则抛出错误 // 如果模板不存在,则抛出错误
if (!template) { if (!template) {
throw new Error(`模板 ID ${id} 不存在`); throw new Error(`模板 ID ${id} 不存在`);
} }
@ -78,22 +78,22 @@ export class TemplateService {
/** /**
* ID * ID
* @param {number} id - ID * @param {number} id - ID
* @returns {Promise<boolean>} true false * @returns {Promise<boolean>} true, false
*/ */
async deleteTemplate(id: number): Promise<boolean> { async deleteTemplate(id: number): Promise<boolean> {
// 首先根据 ID 查找模板 // 首先根据 ID 查找模板
const template = await this.templateModel.findOneBy({ id }); const template = await this.templateModel.findOneBy({ id });
// 如果模板不存在则抛出错误 // 如果模板不存在,则抛出错误
if (!template) { if (!template) {
throw new Error(`模板 ID ${id} 不存在`); throw new Error(`模板 ID ${id} 不存在`);
} }
// 如果模板不可删除则抛出错误 // 如果模板不可删除,则抛出错误
if (!template.deletable) { if (!template.deletable) {
throw new Error(`模板 ${template.name} 不可删除`); throw new Error(`模板 ${template.name} 不可删除`);
} }
// 执行删除操作 // 执行删除操作
const result = await this.templateModel.delete(id); const result = await this.templateModel.delete(id);
// 如果影响的行数大于 0则表示删除成功 // 如果影响的行数大于 0,则表示删除成功
return result.affected > 0; return result.affected > 0;
} }
@ -106,14 +106,14 @@ export class TemplateService {
async render(name: string, data: Record<string, any>): Promise<string> { async render(name: string, data: Record<string, any>): Promise<string> {
// 根据名称获取模板 // 根据名称获取模板
const template = await this.getTemplateByName(name); const template = await this.getTemplateByName(name);
// 如果模板不存在则抛出错误 // 如果模板不存在,则抛出错误
if (!template) { if (!template) {
throw new Error(`模板 '${name}' 不存在`); throw new Error(`模板 '${name}' 不存在`);
} }
// 获取模板的原始内容 // 获取模板的原始内容
let rendered = template.value; let rendered = template.value;
// 遍历数据对象替换模板中的占位符 // 遍历数据对象,替换模板中的占位符
for (const key in data) { for (const key in data) {
// 创建一个正则表达式来匹配 {{key}} // 创建一个正则表达式来匹配 {{key}}
const regex = new RegExp(`{{${key}}}`, 'g'); const regex = new RegExp(`{{${key}}}`, 'g');

View File

@ -138,7 +138,7 @@ export class UniExpressService {
async getOrderStatus(tracking_number: string) { async getOrderStatus(tracking_number: string) {
try { try {
const key = 'SMq45nJhQuNR3WHsJA6N'; // todo写进常数 const key = 'SMq45nJhQuNR3WHsJA6N'; // todo,写进常数
const config: AxiosRequestConfig= { const config: AxiosRequestConfig= {
method: 'GET', method: 'GET',
url: `${this.url}/orders/trackinguniuni`, url: `${this.url}/orders/trackinguniuni`,

View File

@ -61,11 +61,11 @@ export class UserService {
throw new Error('验证码错误'); throw new Error('验证码错误');
} }
// 校验通过后将设备加入白名单 // 校验通过后,将设备加入白名单
await this.deviceWhitelistService.addToWhitelist(deviceId); await this.deviceWhitelistService.addToWhitelist(deviceId);
} }
// 生成 JWT包含角色和权限信息 // 生成 JWT,包含角色和权限信息
const token = await this.jwtService.sign({ const token = await this.jwtService.sign({
id: user.id, id: user.id,
deviceId, deviceId,
@ -81,7 +81,7 @@ export class UserService {
}; };
} }
// 中文注释:新增用户(支持可选备注) // 新增用户(支持可选备注)
async addUser(username: string, password: string, remark?: string) { async addUser(username: string, password: string, remark?: string) {
const existingUser = await this.userModel.findOne({ const existingUser = await this.userModel.findOne({
where: { username }, where: { username },
@ -93,13 +93,13 @@ export class UserService {
const user = this.userModel.create({ const user = this.userModel.create({
username, username,
password: hashedPassword, password: hashedPassword,
// 中文注释:备注字段赋值(若提供) // 备注字段赋值(若提供)
...(remark ? { remark } : {}), ...(remark ? { remark } : {}),
}); });
return this.userModel.save(user); return this.userModel.save(user);
} }
// 中文注释:用户列表支持分页与备注模糊查询(以及可选的布尔过滤) // 用户列表支持分页与备注模糊查询(以及可选的布尔过滤)
async listUsers( async listUsers(
current: number, current: number,
pageSize: number, pageSize: number,
@ -111,13 +111,13 @@ export class UserService {
isAdmin?: boolean; isAdmin?: boolean;
} = {} } = {}
) { ) {
// 条件判断构造 where 条件 // 条件判断:构造 where 条件
const where: Record<string, any> = {}; const where: Record<string, any> = {};
if (filters.username) where.username = Like(`%${filters.username}%`); // 中文注释:用户名精确匹配(如需模糊可改为 Like if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 中文注释:按启用状态过滤 if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 中文注释:按超管过滤 if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 中文注释:按管理员过滤 if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 中文注释:备注模糊搜索 if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
const [items, total] = await this.userModel.findAndCount({ const [items, total] = await this.userModel.findAndCount({
where, where,
@ -137,7 +137,7 @@ export class UserService {
return this.userModel.save(user); return this.userModel.save(user);
} }
// 中文注释:更新用户信息(支持用户名唯一校验与可选密码修改) // 更新用户信息(支持用户名唯一校验与可选密码修改)
async updateUser( async updateUser(
userId: number, userId: number,
payload: { payload: {
@ -149,30 +149,30 @@ export class UserService {
remark?: string; remark?: string;
} }
) { ) {
// 条件判断查询用户是否存在 // 条件判断:查询用户是否存在
const user = await this.userModel.findOne({ where: { id: userId } }); const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) { if (!user) {
throw new Error('User not found'); throw new Error('User not found');
} }
// 条件判断:若提供了新用户名且与原用户名不同,校验唯一性 // 条件判断:若提供了新用户名且与原用户名不同,校验唯一性
if (payload.username && payload.username !== user.username) { if (payload.username && payload.username !== user.username) {
const exist = await this.userModel.findOne({ where: { username: payload.username } }); const exist = await this.userModel.findOne({ where: { username: payload.username } });
if (exist) throw new Error('用户名已存在'); if (exist) throw new Error('用户名已存在');
user.username = payload.username; user.username = payload.username;
} }
// 条件判断若提供密码则进行加密存储 // 条件判断:若提供密码则进行加密存储
if (payload.password) { if (payload.password) {
user.password = await bcrypt.hash(payload.password, 10); user.password = await bcrypt.hash(payload.password, 10);
} }
// 条件判断:更新布尔与权限字段(若提供则覆盖) // 条件判断:更新布尔与权限字段(若提供则覆盖)
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper; if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin; if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
if (Array.isArray(payload.permissions)) user.permissions = payload.permissions; if (Array.isArray(payload.permissions)) user.permissions = payload.permissions;
// 条件判断:更新备注(若提供则覆盖) // 条件判断:更新备注(若提供则覆盖)
if (typeof payload.remark === 'string') user.remark = payload.remark; if (typeof payload.remark === 'string') user.remark = payload.remark;
// 保存更新 // 保存更新

View File

@ -13,11 +13,11 @@ export class WPService {
private readonly siteService: SiteService; private readonly siteService: SiteService;
/** /**
* URL / / * URL,, / /
* 使this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId) * 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId)
*/ */
private buildURL(base: string, ...parts: Array<string | number>): string { private buildURL(base: string, ...parts: Array<string | number>): string {
// 去掉 base 末尾多余斜杠但不影响协议中的 // // 去掉 base 末尾多余斜杠,但不影响协议中的 //
const baseSanitized = String(base).replace(/\/+$/g, ''); const baseSanitized = String(base).replace(/\/+$/g, '');
// 规范各段前后斜杠 // 规范各段前后斜杠
const segments = parts const segments = parts
@ -26,14 +26,14 @@ export class WPService {
.map((s) => s.replace(/^\/+|\/+$/g, '')) .map((s) => s.replace(/^\/+|\/+$/g, ''))
.filter(Boolean); .filter(Boolean);
const joined = [baseSanitized, ...segments].join('/'); const joined = [baseSanitized, ...segments].join('/');
// 折叠除协议外的多余斜杠例如 https://example.com//a///b -> https://example.com/a/b // 折叠除协议外的多余斜杠,例如 https://example.com//a///b -> https://example.com/a/b
return joined.replace(/([^:])\/{2,}/g, '$1/'); return joined.replace(/([^:])\/{2,}/g, '$1/');
} }
/** /**
* WooCommerce SDK * WooCommerce SDK
* @param site * @param site
* @param namespace API wc/v3 wcs/v1 * @param namespace API , wc/v3; wcs/v1
*/ */
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
return new WooCommerceRestApi({ return new WooCommerceRestApi({
@ -45,14 +45,14 @@ export class WPService {
} }
/** /**
* SDK totalPages * SDK , totalPages
*/ */
private async sdkGetPage<T>(api: any, resource: string, params: Record<string, any> = {}) { private async sdkGetPage<T>(api: any, resource: string, params: Record<string, any> = {}) {
const page = params.page ?? 1; const page = params.page ?? 1;
const per_page = params.per_page ?? 100; const per_page = params.per_page ?? 100;
const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page }); const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page });
if (res?.headers?.['content-type']?.includes('text/html')) { if (res?.headers?.['content-type']?.includes('text/html')) {
throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限'); throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限');
} }
const data = res.data as T[]; const data = res.data as T[];
const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1);
@ -61,7 +61,7 @@ export class WPService {
} }
/** /**
* SDK * SDK ,
*/ */
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> { private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
const result: T[] = []; const result: T[] = [];
@ -76,7 +76,7 @@ export class WPService {
/** /**
* WordPress * WordPress
* @param wpApiUrl WordPress REST API * @param wpApiUrl WordPress REST API
* @param endpoint API wc/v3/products * @param endpoint API ( wc/v3/products)
* @param consumerKey WooCommerce * @param consumerKey WooCommerce
* @param consumerSecret WooCommerce * @param consumerSecret WooCommerce
*/ */
@ -91,7 +91,7 @@ export class WPService {
try { try {
const apiUrl = site.apiUrl; const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site; const { consumerKey, consumerSecret } = site;
// 构建 URL规避多/或少/问题 // 构建 URL,规避多/或少/问题
const url = this.buildURL(apiUrl, '/wp-json', endpoint); const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
@ -126,7 +126,7 @@ export class WPService {
while (hasMore) { while (hasMore) {
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'GET', method: 'GET',
// 构建 URL规避多/或少/问题 // 构建 URL,规避多/或少/问题
url: this.buildURL(apiUrl, '/wp-json', endpoint), url: this.buildURL(apiUrl, '/wp-json', endpoint),
headers: { headers: {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
@ -194,12 +194,12 @@ export class WPService {
/** /**
* WooCommerce Subscriptions * WooCommerce Subscriptions
* wc/v1/subscriptionsSubscriptions 退 wc/v3/subscriptions * wc/v1/subscriptions(Subscriptions ),退 wc/v3/subscriptions.
* * .
*/ */
async getSubscriptions(siteId: number): Promise<Record<string, any>[]> { async getSubscriptions(siteId: number): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId); const site = await this.siteService.get(siteId);
// 优先使用 Subscriptions 命名空间 wcs/v1失败回退 wc/v3 // 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
const api = this.createApi(site, 'wc/v3'); const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions'); return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions');
@ -257,7 +257,7 @@ export class WPService {
); );
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'PUT', method: 'PUT',
// 构建 URL规避多/或少/问题 // 构建 URL,规避多/或少/问题
url: this.buildURL(apiUrl, '/wp-json', endpoint), url: this.buildURL(apiUrl, '/wp-json', endpoint),
headers: { headers: {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
@ -304,7 +304,7 @@ export class WPService {
): Promise<Boolean> { ): Promise<Boolean> {
const res = await this.updateData(`/wc/v3/products/${productId}`, site, { const res = await this.updateData(`/wc/v3/products/${productId}`, site, {
status, status,
manage_stock: false, // 为true的时候用quantity控制库存为false时直接用stock_status控制 manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
stock_status, stock_status,
}); });
return res; return res;
@ -357,7 +357,7 @@ export class WPService {
); );
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'POST', method: 'POST',
// 构建 URL规避多/或少/问题 // 构建 URL,规避多/或少/问题
url: this.buildURL( url: this.buildURL(
apiUrl, apiUrl,
'/wp-json', '/wp-json',
@ -385,10 +385,10 @@ export class WPService {
); );
console.log('del', orderId, trackingId); console.log('del', orderId, trackingId);
// 删除接口 DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id> // 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'DELETE', method: 'DELETE',
// 构建 URL规避多/或少/问题 // 构建 URL,规避多/或少/问题
url: this.buildURL( url: this.buildURL(
apiUrl, apiUrl,
'/wp-json', '/wp-json',

View File

@ -15,7 +15,7 @@ import { SiteService } from './site.service';
@Provide() @Provide()
export class WpProductService { export class WpProductService {
// 移除配置中的站点数组统一从数据库获取站点信息 // 移除配置中的站点数组,统一从数据库获取站点信息
@Inject() @Inject()
private readonly wpApiService: WPService; private readonly wpApiService: WPService;
@ -31,7 +31,7 @@ export class WpProductService {
async syncAllSites() { async syncAllSites() {
// 从数据库获取所有启用的站点并逐站点同步产品与变体 // 从数据库获取所有启用的站点,并逐站点同步产品与变体
const { items: sites } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true); const { items: sites } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true);
for (const site of sites) { for (const site of sites) {
const products = await this.wpApiService.getProducts(site); const products = await this.wpApiService.getProducts(site);
@ -46,7 +46,7 @@ export class WpProductService {
} }
// 同步一个网站 // 同步一个网站
async syncSite(siteId: number) { async syncSite(siteId: number) {
// 通过数据库获取站点并转换为 WpSite用于后续 WooCommerce 同步 // 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
const site = await this.siteService.get(siteId, true); const site = await this.siteService.get(siteId, true);
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product') const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
.select([ .select([
@ -304,7 +304,7 @@ export class WpProductService {
async getProductList(param: QueryWpProductDTO) { async getProductList(param: QueryWpProductDTO) {
const { current = 1, pageSize = 10, name, siteId, status } = param; const { current = 1, pageSize = 10, name, siteId, status } = param;
// 第一步先查询分页的产品 // 第一步:先查询分页的产品
const where: any = {}; const where: any = {};
if (siteId) { if (siteId) {
where.siteId = siteId; where.siteId = siteId;
@ -376,9 +376,9 @@ export class WpProductService {
const items = rawResult.reduce((acc, row) => { const items = rawResult.reduce((acc, row) => {
// 在累加器中查找当前产品 // 在累加器中查找当前产品
let product = acc.find(p => p.id === row.id); let product = acc.find(p => p.id === row.id);
// 如果产品不存在则创建新产品 // 如果产品不存在,则创建新产品
if (!product) { if (!product) {
// 从原始产品列表中查找以获取 'site' 关联数据 // 从原始产品列表中查找,以获取 'site' 关联数据
const originalProduct = products.find(p => p.id === row.id); const originalProduct = products.find(p => p.id === row.id);
product = { product = {
...Object.keys(row) ...Object.keys(row)
@ -535,7 +535,7 @@ export class WpProductService {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
const conditions: string[] = []; const conditions: string[] = [];
// 英文名关键词全部匹配AND // 英文名关键词全部匹配(AND)
if (nameFilter.length > 0) { if (nameFilter.length > 0) {
const nameConds = nameFilter.map((word, index) => { const nameConds = nameFilter.map((word, index) => {
const key = `name${index}`; const key = `name${index}`;

View File

@ -24,7 +24,7 @@ export async function paginate<T>(
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[]; where?: FindOptionsWhere<T> | FindOptionsWhere<T>[];
relations?: string[]; relations?: string[];
order?: Record<string, 'ASC' | 'DESC'>; order?: Record<string, 'ASC' | 'DESC'>;
transformerClass?: new () => T; // 可选用于指定需要转换的类 transformerClass?: new () => T; // 可选:用于指定需要转换的类
} }
): Promise<PaginationResult<T>> { ): Promise<PaginationResult<T>> {
const { const {

View File

@ -17,7 +17,7 @@
"typeRoots": ["./typings", "./node_modules/@types"], "typeRoots": ["./typings", "./node_modules/@types"],
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src",
"inlineSources": true // map 便 VS Code "inlineSources": true // map ,便 VS Code
}, },
"exclude": ["*.js", "*.ts", "dist", "node_modules", "test"] "exclude": ["*.js", "*.ts", "dist", "node_modules", "test"]