Compare commits
7 Commits
f20f4727f6
...
62f9ca947a
| Author | SHA1 | Date |
|---|---|---|
|
|
62f9ca947a | |
|
|
d91ec7bc60 | |
|
|
4bb0988034 | |
|
|
4bbfa0cc2d | |
|
|
998e1e31c7 | |
|
|
b8aee530e8 | |
|
|
0180360519 |
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "./node_modules/mwts/",
|
||||
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
||||
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings", "scripts"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,3 +15,5 @@ yarn.lock
|
|||
**/config.prod.ts
|
||||
**/config.local.ts
|
||||
container
|
||||
scripts
|
||||
ai
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 概述
|
||||
|
||||
本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能。
|
||||
本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能.
|
||||
|
||||
## API 接口列表
|
||||
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
**参数说明**
|
||||
- `name`: 区域名称 (必填)
|
||||
- `latitude`: 纬度 (-90 到 90 之间,可选)
|
||||
- `longitude`: 经度 (-180 到 180 之间,可选)
|
||||
- `latitude`: 纬度 (-90 到 90 之间,可选)
|
||||
- `longitude`: 经度 (-180 到 180 之间,可选)
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
|
|
@ -61,8 +61,8 @@
|
|||
|
||||
**参数说明**
|
||||
- `name`: 区域名称 (可选)
|
||||
- `latitude`: 纬度 (-90 到 90 之间,可选)
|
||||
- `longitude`: 经度 (-180 到 180 之间,可选)
|
||||
- `latitude`: 纬度 (-90 到 90 之间,可选)
|
||||
- `longitude`: 经度 (-180 到 180 之间,可选)
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
}
|
||||
```
|
||||
|
||||
### 4. 获取区域列表(分页)
|
||||
### 4. 获取区域列表(分页)
|
||||
|
||||
**请求信息**
|
||||
- URL: `/api/area`
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
- Query Parameters:
|
||||
- `currentPage`: 当前页码 (默认 1)
|
||||
- `pageSize`: 每页数量 (默认 10)
|
||||
- `name`: 区域名称(可选,用于搜索)
|
||||
- `name`: 区域名称(可选,用于搜索)
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
|
|
@ -186,11 +186,11 @@
|
|||
|
||||
## 世界地图实现建议
|
||||
|
||||
对于前端实现世界地图并显示区域坐标,推荐以下方案:
|
||||
对于前端实现世界地图并显示区域坐标,推荐以下方案:
|
||||
|
||||
### 1. 使用开源地图库
|
||||
|
||||
- **Leaflet.js**: 轻量级开源地图库,易于集成
|
||||
- **Leaflet.js**: 轻量级开源地图库,易于集成
|
||||
- **Mapbox**: 提供丰富的地图样式和交互功能
|
||||
- **Google Maps API**: 功能强大但需要API密钥
|
||||
|
||||
|
|
@ -222,15 +222,15 @@
|
|||
4. **添加交互功能**:
|
||||
- 点击标记显示区域详情
|
||||
- 搜索和筛选功能
|
||||
- 编辑坐标功能(调用更新API)
|
||||
- 编辑坐标功能(调用更新API)
|
||||
|
||||
### 3. 坐标输入建议
|
||||
|
||||
在区域管理界面,可以添加以下功能来辅助坐标输入:
|
||||
在区域管理界面,可以添加以下功能来辅助坐标输入:
|
||||
|
||||
1. 提供搜索框,根据地点名称自动获取坐标
|
||||
2. 集成小型地图,允许用户点击选择位置
|
||||
3. 添加验证,确保输入的坐标在有效范围内
|
||||
1. 提供搜索框,根据地点名称自动获取坐标
|
||||
2. 集成小型地图,允许用户点击选择位置
|
||||
3. 添加验证,确保输入的坐标在有效范围内
|
||||
|
||||
## 数据模型说明
|
||||
|
||||
|
|
@ -238,16 +238,16 @@
|
|||
|
||||
| 字段名 | 类型 | 描述 | 是否必填 |
|
||||
|--------|------|------|----------|
|
||||
| id | number | 区域ID | 否(自动生成) |
|
||||
| id | number | 区域ID | 否(自动生成) |
|
||||
| name | string | 区域名称 | 是 |
|
||||
| latitude | number | 纬度(范围:-90 到 90) | 否 |
|
||||
| longitude | number | 经度(范围:-180 到 180) | 否 |
|
||||
| createdAt | Date | 创建时间 | 否(自动生成) |
|
||||
| updatedAt | Date | 更新时间 | 否(自动生成) |
|
||||
| latitude | number | 纬度(范围:-90 到 90) | 否 |
|
||||
| longitude | number | 经度(范围:-180 到 180) | 否 |
|
||||
| createdAt | Date | 创建时间 | 否(自动生成) |
|
||||
| updatedAt | Date | 更新时间 | 否(自动生成) |
|
||||
|
||||
## 错误处理
|
||||
|
||||
API可能返回的错误信息:
|
||||
API可能返回的错误信息:
|
||||
|
||||
- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
|
||||
- `区域不存在`: 当尝试更新或删除不存在的区域时
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# 数据库迁移指南
|
||||
|
||||
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude` 和 `longitude` 字段添加到数据库表中。
|
||||
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude` 和 `longitude` 字段添加到数据库表中.
|
||||
|
||||
## 执行迁移步骤
|
||||
|
||||
### 1. 生成迁移文件
|
||||
|
||||
运行以下命令生成迁移文件:
|
||||
运行以下命令生成迁移文件:
|
||||
|
||||
```bash
|
||||
npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
|
||||
|
|
@ -14,19 +14,19 @@ npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
|
|||
|
||||
### 2. 检查迁移文件
|
||||
|
||||
生成的迁移文件会自动包含添加 `latitude` 和 `longitude` 字段的SQL语句。您可以检查文件内容确保迁移逻辑正确。
|
||||
生成的迁移文件会自动包含添加 `latitude` 和 `longitude` 字段的SQL语句.您可以检查文件内容确保迁移逻辑正确.
|
||||
|
||||
### 3. 执行迁移
|
||||
|
||||
运行以下命令执行迁移,将更改应用到数据库:
|
||||
运行以下命令执行迁移,将更改应用到数据库:
|
||||
|
||||
```bash
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
## 手动迁移SQL(可选)
|
||||
## 手动迁移SQL(可选)
|
||||
|
||||
如果需要手动执行SQL,可以使用以下语句:
|
||||
如果需要手动执行SQL,可以使用以下语句:
|
||||
|
||||
```sql
|
||||
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`;
|
||||
```
|
||||
|
||||
## 回滚迁移(如需)
|
||||
## 回滚迁移(如需)
|
||||
|
||||
如果遇到问题,可以使用以下命令回滚迁移:
|
||||
如果遇到问题,可以使用以下命令回滚迁移:
|
||||
|
||||
```bash
|
||||
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
|
||||
- 迁移后,可以通过API开始为区域添加坐标信息
|
||||
- 迁移不会影响现有数据,新增字段默认为 NULL
|
||||
- 迁移后,可以通过API开始为区域添加坐标信息
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"@midwayjs/logger": "^3.1.0",
|
||||
"@midwayjs/swagger": "^3.20.2",
|
||||
"@midwayjs/typeorm": "^3.20.0",
|
||||
"@midwayjs/upload": "^3.20.16",
|
||||
"@midwayjs/validate": "^3.20.2",
|
||||
"@woocommerce/woocommerce-rest-api": "^1.0.2",
|
||||
"axios": "^1.13.2",
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"class-transformer": "^0.5.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"eta": "^4.4.1",
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"mysql2": "^3.15.3",
|
||||
"nodemailer": "^7.0.5",
|
||||
|
|
@ -813,6 +815,19 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@midwayjs/upload": {
|
||||
"version": "3.20.16",
|
||||
"resolved": "https://registry.npmjs.org/@midwayjs/upload/-/upload-3.20.16.tgz",
|
||||
"integrity": "sha512-lnHDOeU4wGvABJjYjSR5dpG+6f4+hxJhU+1TsT+OsrKMDju6AyTOnTSFa1V1vMO7+VplKzkWP+ZFJkAVI0Buuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-type": "16.5.4",
|
||||
"raw-body": "2.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@midwayjs/validate": {
|
||||
"version": "3.20.13",
|
||||
"resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.13.tgz",
|
||||
|
|
@ -928,6 +943,12 @@
|
|||
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz",
|
||||
|
|
@ -1153,6 +1174,18 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
|
|
@ -2246,6 +2279,36 @@
|
|||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eta": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eta/-/eta-4.4.1.tgz",
|
||||
"integrity": "sha512-4o6fYxhRmFmO9SJcU9PxBLYPGapvJ/Qha0ZE+Y6UE9QIUd0Wk1qaLISQ6J1bM7nOcWHhs1YmY3mfrfwkJRBTWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/bgub/eta?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
|
|
@ -2278,6 +2341,23 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/file-type": {
|
||||
"version": "16.5.4",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
|
||||
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-web-to-node-stream": "^3.0.0",
|
||||
"strtok3": "^6.2.4",
|
||||
"token-types": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
||||
|
|
@ -3607,6 +3687,19 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/peek-readable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
|
||||
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
|
||||
|
|
@ -3641,6 +3734,15 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
|
|
@ -3777,6 +3879,47 @@
|
|||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
|
||||
"integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^4.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-web-to-node-stream/node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
|
||||
|
|
@ -4302,6 +4445,23 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"peek-readable": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
|
||||
|
|
@ -4402,6 +4562,23 @@
|
|||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/token-types": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
|
||||
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tokenizer/token": "^0.3.0",
|
||||
"ieee754": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/tsc-alias": {
|
||||
"version": "1.8.16",
|
||||
"resolved": "https://registry.npmmirror.com/tsc-alias/-/tsc-alias-1.8.16.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@midwayjs/logger": "^3.1.0",
|
||||
"@midwayjs/swagger": "^3.20.2",
|
||||
"@midwayjs/typeorm": "^3.20.0",
|
||||
"@midwayjs/upload": "^3.20.16",
|
||||
"@midwayjs/validate": "^3.20.2",
|
||||
"@woocommerce/woocommerce-rest-api": "^1.0.2",
|
||||
"axios": "^1.13.2",
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
"class-transformer": "^0.5.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"eta": "^4.4.1",
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"mysql2": "^3.15.3",
|
||||
"nodemailer": "^7.0.5",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ import { DictItem } from '../entity/dict_item.entity';
|
|||
import { Template } from '../entity/template.entity';
|
||||
import { Area } from '../entity/area.entity';
|
||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||
import { Category } from '../entity/category.entity';
|
||||
import DictSeeder from '../db/seeds/dict.seeder';
|
||||
import CategorySeeder from '../db/seeds/category.seeder';
|
||||
import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder';
|
||||
|
||||
export default {
|
||||
// use for cookie sign key, should change to your own and keep security
|
||||
|
|
@ -81,16 +85,18 @@ export default {
|
|||
DictItem,
|
||||
Template,
|
||||
Area,
|
||||
CategoryAttribute,
|
||||
Category,
|
||||
],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
seeders: [DictSeeder],
|
||||
seeders: [DictSeeder, CategorySeeder, CategoryAttributeSeeder],
|
||||
},
|
||||
dataSource: {
|
||||
default: {
|
||||
type: 'mysql',
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
port: 10014,
|
||||
username: 'root',
|
||||
password: 'root',
|
||||
database: 'inventory',
|
||||
|
|
@ -101,7 +107,7 @@ export default {
|
|||
// origin: '*', // 允许所有来源跨域请求
|
||||
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
||||
// allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
||||
// credentials: true, // 允许携带凭据(cookies等)
|
||||
// credentials: true, // 允许携带凭据(cookies等)
|
||||
// },
|
||||
// jwt: {
|
||||
// secret: 'YOONE2024!@abc',
|
||||
|
|
@ -134,5 +140,11 @@ export default {
|
|||
user: 'info@canpouches.com',
|
||||
pass: 'WWqQ4aZq4Jrm9uwz',
|
||||
},
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
// mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
|
||||
mode: 'file',
|
||||
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
||||
whitelist: ['.csv'], // 支持的文件后缀
|
||||
},
|
||||
} as MidwayConfig;
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ export default {
|
|||
dataSource: {
|
||||
default: {
|
||||
host: 'localhost',
|
||||
port: "23306",
|
||||
username: 'root',
|
||||
password: '12345678',
|
||||
database: 'inventory',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -25,7 +27,7 @@ export default {
|
|||
origin: '*', // 允许所有来源跨域请求
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
||||
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
||||
credentials: true, // 允许携带凭据(cookies等)
|
||||
credentials: true, // 允许携带凭据(cookies等)
|
||||
},
|
||||
jwt: {
|
||||
secret: 'YOONE2024!@abc',
|
||||
|
|
@ -33,30 +35,34 @@ export default {
|
|||
},
|
||||
wpSite: [
|
||||
{
|
||||
id: '-1',
|
||||
siteName: 'Admin',
|
||||
email: '2469687281@qq.com',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
wpApiUrl: 'http://t2-shop.local/',
|
||||
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||
siteName: 'Local',
|
||||
email: '2469687281@qq.com',
|
||||
emailPswd: 'lulin91.',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
wpApiUrl: 'http://t1-shop.local/',
|
||||
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||
siteName: 'Local-test-2',
|
||||
id: '200',
|
||||
wpApiUrl: "http://simple.local",
|
||||
consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886',
|
||||
consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8',
|
||||
siteName: 'LocalSimple',
|
||||
email: '2469687281@qq.com',
|
||||
emailPswd: 'lulin91.',
|
||||
},
|
||||
// {
|
||||
// id: '2',
|
||||
// wpApiUrl: 'http://t2-shop.local/',
|
||||
// consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||
// consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||
// siteName: 'Local',
|
||||
// email: '2469687281@qq.com',
|
||||
// emailPswd: 'lulin91.',
|
||||
// },
|
||||
// {
|
||||
// id: '3',
|
||||
// wpApiUrl: 'http://t1-shop.local/',
|
||||
// consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||
// consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||
// siteName: 'Local-test-2',
|
||||
// email: '2469687281@qq.com',
|
||||
// emailPswd: 'lulin91.',
|
||||
// },
|
||||
// {
|
||||
// id: '2',
|
||||
// wpApiUrl: 'http://localhost:10004',
|
||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import * as swagger from '@midwayjs/swagger';
|
|||
import * as crossDomain from '@midwayjs/cross-domain';
|
||||
import * as cron from '@midwayjs/cron';
|
||||
import * as jwt from '@midwayjs/jwt';
|
||||
import * as upload from '@midwayjs/upload';
|
||||
import { USER_KEY } from './decorator/user.decorator';
|
||||
import { SiteService } from './service/site.service';
|
||||
import { AuthMiddleware } from './middleware/auth.middleware';
|
||||
|
|
@ -33,6 +34,7 @@ import { AuthMiddleware } from './middleware/auth.middleware';
|
|||
crossDomain,
|
||||
cron,
|
||||
jwt,
|
||||
upload,
|
||||
],
|
||||
importConfigs: [join(__dirname, './config')],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export class AreaController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '获取区域列表(分页)' })
|
||||
@ApiOperation({ summary: '获取区域列表(分页)' })
|
||||
@ApiOkResponse({ type: [Area], description: '区域列表' })
|
||||
@ApiExtension('x-pagination', { currentPage: 1, pageSize: 10, total: 100 })
|
||||
@Get('/')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { Controller, Get, Post, Put, Del, Body, Query, Inject, Param } from '@midwayjs/core';
|
||||
import { CategoryService } from '../service/category.service';
|
||||
import { successResponse, errorResponse } from '../utils/response.util';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
|
||||
@Controller('/category')
|
||||
export class CategoryController {
|
||||
@Inject()
|
||||
categoryService: CategoryService;
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/all')
|
||||
async getAll() {
|
||||
try {
|
||||
const data = await this.categoryService.getAll();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/')
|
||||
async getList(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
const data = await this.categoryService.getList({ current, pageSize }, name);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('/')
|
||||
async create(@Body() body: any) {
|
||||
try {
|
||||
const data = await this.categoryService.create(body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Put('/:id')
|
||||
async update(@Param('id') id: number, @Body() body: any) {
|
||||
try {
|
||||
const data = await this.categoryService.update(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Del('/:id')
|
||||
async delete(@Param('id') id: number) {
|
||||
try {
|
||||
await this.categoryService.delete(id);
|
||||
return successResponse(null);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/attribute/:categoryId')
|
||||
async getCategoryAttributes(@Param('categoryId') categoryId: number) {
|
||||
try {
|
||||
const data = await this.categoryService.getCategoryAttributes(categoryId);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('/attribute')
|
||||
async createCategoryAttribute(@Body() body: { categoryId: number, attributeDictIds: number[] }) {
|
||||
try {
|
||||
const data = await this.categoryService.createCategoryAttribute(body.categoryId, body.attributeDictIds);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Del('/attribute/:id')
|
||||
async deleteCategoryAttribute(@Param('id') id: number) {
|
||||
try {
|
||||
await this.categoryService.deleteCategoryAttribute(id);
|
||||
return successResponse(null);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export class DictController {
|
|||
*/
|
||||
@Get('/:id')
|
||||
async getDict(@Param('id') id: number) {
|
||||
// 调用服务层方法,并关联查询字典项
|
||||
// 调用服务层方法,并关联查询字典项
|
||||
return this.dictService.getDict({ id }, ['items']);
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export class DictController {
|
|||
/**
|
||||
* 批量导入字典项
|
||||
* @param files 上传的文件
|
||||
* @param body 请求体,包含字典ID
|
||||
* @param body 请求体,包含字典ID
|
||||
*/
|
||||
@Post('/item/import')
|
||||
@Validate()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export class LocaleController {
|
|||
|
||||
/**
|
||||
* 根据语言获取所有翻译项
|
||||
* @param lang 语言代码,例如 zh-CN, en-US
|
||||
* @param lang 语言代码,例如 zh-CN, en-US
|
||||
* @returns 返回一个包含所有翻译键值对的 JSON 对象
|
||||
*/
|
||||
@Get('/:lang')
|
||||
|
|
@ -20,7 +20,7 @@ export class LocaleController {
|
|||
// 根据语言代码查找对应的字典
|
||||
const dict = await this.dictService.getDict({ name: lang }, ['items']);
|
||||
|
||||
// 如果字典不存在,则返回空对象
|
||||
// 如果字典不存在,则返回空对象
|
||||
if (!dict) {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { Controller, Get, Inject, Query } from '@midwayjs/core';
|
||||
import { WPService } from '../service/wp.service';
|
||||
import { successResponse, errorResponse } from '../utils/response.util';
|
||||
|
||||
@Controller('/media')
|
||||
export class MediaController {
|
||||
@Inject()
|
||||
wpService: WPService;
|
||||
|
||||
@Get('/list')
|
||||
async list(
|
||||
@Query('siteId') siteId: number,
|
||||
@Query('page') page: number = 1,
|
||||
@Query('pageSize') pageSize: number = 20
|
||||
) {
|
||||
try {
|
||||
if (!siteId) {
|
||||
return errorResponse('siteId is required');
|
||||
}
|
||||
const result = await this.wpService.getMedia(siteId, page, pageSize);
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,10 @@ import {
|
|||
Query,
|
||||
Controller,
|
||||
} from '@midwayjs/core';
|
||||
import * as fs from 'fs';
|
||||
import { ProductService } from '../service/product.service';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto';
|
||||
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO, BatchUpdateProductDTO } from '../dto/product.dto';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
||||
import { ContentType, Files } from '@midwayjs/core';
|
||||
|
|
@ -21,7 +22,6 @@ import { Context } from '@midwayjs/koa';
|
|||
export class ProductController {
|
||||
@Inject()
|
||||
productService: ProductService;
|
||||
ProductRes;
|
||||
|
||||
@Inject()
|
||||
ctx: Context;
|
||||
|
|
@ -63,12 +63,14 @@ export class ProductController {
|
|||
async getProductList(
|
||||
@Query() query: QueryProductDTO
|
||||
): Promise<ProductListRes> {
|
||||
const { current = 1, pageSize = 10, name, brandId } = query;
|
||||
const { current = 1, pageSize = 10, name, brandId, sortField, sortOrder } = query;
|
||||
try {
|
||||
const data = await this.productService.getProductList(
|
||||
{ current, pageSize },
|
||||
name,
|
||||
brandId
|
||||
brandId,
|
||||
sortField,
|
||||
sortOrder
|
||||
);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -88,14 +90,14 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 中文注释:导出所有产品 CSV
|
||||
// 导出所有产品 CSV
|
||||
@ApiOkResponse()
|
||||
@Get('/export')
|
||||
@ContentType('text/csv')
|
||||
async exportProductsCSV() {
|
||||
try {
|
||||
const csv = await this.productService.exportProductsCSV();
|
||||
// 设置下载文件名(中文注释:附件形式)
|
||||
// 设置下载文件名(附件形式)
|
||||
const date = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const name = `products-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}.csv`;
|
||||
|
|
@ -106,15 +108,26 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 中文注释:导入产品(CSV 文件)
|
||||
// 导入产品(CSV 文件)
|
||||
@ApiOkResponse()
|
||||
@Post('/import')
|
||||
async importProductsCSV(@Files() files: any) {
|
||||
try {
|
||||
// 条件判断:确保存在文件
|
||||
// 条件判断:确保存在文件
|
||||
const file = files?.[0];
|
||||
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);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -132,6 +145,17 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Put('/batch-update')
|
||||
async batchUpdateProduct(@Body() batchUpdateProductDTO: BatchUpdateProductDTO) {
|
||||
try {
|
||||
await this.productService.batchUpdateProduct(batchUpdateProductDTO);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: ProductRes })
|
||||
@Put('updateNameCn/:id/:nameCn')
|
||||
async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
||||
|
|
@ -154,7 +178,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 中文注释:获取产品的库存组成
|
||||
// 获取产品的库存组成
|
||||
@ApiOkResponse()
|
||||
@Get('/:id/components')
|
||||
async getProductComponents(@Param('id') id: number) {
|
||||
|
|
@ -166,19 +190,19 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 中文注释:设置产品的库存组成(覆盖式)
|
||||
// 设置产品的库存组成(覆盖式)
|
||||
@ApiOkResponse()
|
||||
@Post('/:id/components')
|
||||
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
|
||||
try {
|
||||
const data = await this.productService.setProductComponents(id, body?.items || []);
|
||||
const data = await this.productService.setProductComponents(id, body?.components || []);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 中文注释:根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
|
||||
// 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
|
||||
@ApiOkResponse()
|
||||
@Post('/:id/components/auto')
|
||||
async autoBindComponents(@Param('id') id: number) {
|
||||
|
|
@ -191,7 +215,21 @@ export class ProductController {
|
|||
}
|
||||
|
||||
|
||||
// 通用属性接口:分页列表
|
||||
// 获取所有 WordPress 商品
|
||||
@ApiOkResponse({ description: '获取所有 WordPress 商品' })
|
||||
@Get('/wp-products')
|
||||
async getWpProducts() {
|
||||
try {
|
||||
const data = await this.productService.getWpProducts();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 通用属性接口:分页列表
|
||||
@ApiOkResponse()
|
||||
@Get('/attribute')
|
||||
async getAttributeList(
|
||||
|
|
@ -212,7 +250,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 通用属性接口:全部列表
|
||||
// 通用属性接口:全部列表
|
||||
@ApiOkResponse()
|
||||
@Get('/attributeAll')
|
||||
async getAttributeAll(@Query('dictName') dictName: string) {
|
||||
|
|
@ -224,7 +262,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 通用属性接口:创建
|
||||
// 通用属性接口:创建
|
||||
@ApiOkResponse()
|
||||
@Post('/attribute')
|
||||
async createAttribute(
|
||||
|
|
@ -232,7 +270,7 @@ export class ProductController {
|
|||
@Body() body: { title: string; name: string }
|
||||
) {
|
||||
try {
|
||||
// 调用 getOrCreateAttribute 方法,如果不存在则创建,如果存在则返回
|
||||
// 调用 getOrCreateAttribute 方法,如果不存在则创建,如果存在则返回
|
||||
const data = await this.productService.getOrCreateAttribute(dictName, body.title, body.name);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -240,7 +278,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 通用属性接口:更新
|
||||
// 通用属性接口:更新
|
||||
@ApiOkResponse()
|
||||
@Put('/attribute/:id')
|
||||
async updateAttribute(
|
||||
|
|
@ -264,7 +302,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 通用属性接口:删除
|
||||
// 通用属性接口:删除
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/attribute/:id')
|
||||
async deleteAttribute(@Param('id') id: number) {
|
||||
|
|
@ -276,12 +314,12 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 兼容旧接口:品牌
|
||||
// 兼容旧接口:品牌
|
||||
@ApiOkResponse()
|
||||
@Get('/brandAll')
|
||||
async compatBrandAll() {
|
||||
try {
|
||||
const data = await this.productService.getAttributeAll('brand'); // 中文注释:返回所有品牌字典项
|
||||
const data = await this.productService.getAttributeAll('brand'); // 返回所有品牌字典项
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -292,7 +330,7 @@ export class ProductController {
|
|||
@Get('/brands')
|
||||
async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 中文注释:分页品牌列表
|
||||
const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 分页品牌列表
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -303,9 +341,9 @@ export class ProductController {
|
|||
@Post('/brand')
|
||||
async compatCreateBrand(@Body() body: { title: string; name: string }) {
|
||||
try {
|
||||
const has = await this.productService.hasAttribute('brand', body.name); // 中文注释:唯一性校验
|
||||
const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
|
||||
if (has) return errorResponse('品牌已存在');
|
||||
const data = await this.productService.createAttribute('brand', body); // 中文注释:创建品牌字典项
|
||||
const data = await this.productService.createAttribute('brand', body); // 创建品牌字典项
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -317,10 +355,10 @@ export class ProductController {
|
|||
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||
try {
|
||||
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('品牌已存在');
|
||||
}
|
||||
const data = await this.productService.updateAttribute(id, body); // 中文注释:更新品牌字典项
|
||||
const data = await this.productService.updateAttribute(id, body); // 更新品牌字典项
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -331,14 +369,14 @@ export class ProductController {
|
|||
@Del('/brand/:id')
|
||||
async compatDeleteBrand(@Param('id') id: number) {
|
||||
try {
|
||||
await this.productService.deleteAttribute(id); // 中文注释:删除品牌字典项
|
||||
await this.productService.deleteAttribute(id); // 删除品牌字典项
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧接口:口味
|
||||
// 兼容旧接口:口味
|
||||
@ApiOkResponse()
|
||||
@Get('/flavorsAll')
|
||||
async compatFlavorsAll() {
|
||||
|
|
@ -400,7 +438,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 兼容旧接口:规格
|
||||
// 兼容旧接口:规格
|
||||
@ApiOkResponse()
|
||||
@Get('/strengthAll')
|
||||
async compatStrengthAll() {
|
||||
|
|
@ -462,7 +500,7 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
// 兼容旧接口:尺寸
|
||||
// 兼容旧接口:尺寸
|
||||
@ApiOkResponse()
|
||||
@Get('/sizeAll')
|
||||
async compatSizeAll() {
|
||||
|
|
@ -523,4 +561,88 @@ export class ProductController {
|
|||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有分类
|
||||
@ApiOkResponse({ description: '获取所有分类' })
|
||||
@Get('/categories/all')
|
||||
async getCategoriesAll() {
|
||||
try {
|
||||
const data = await this.productService.getCategoriesAll();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类下的属性配置
|
||||
@ApiOkResponse({ description: '获取分类下的属性配置' })
|
||||
@Get('/category/:id/attributes')
|
||||
async getCategoryAttributes(@Param('id') id: number) {
|
||||
try {
|
||||
const data = await this.productService.getCategoryAttributes(id);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建分类
|
||||
@ApiOkResponse({ description: '创建分类' })
|
||||
@Post('/category')
|
||||
async createCategory(@Body() body: any) {
|
||||
try {
|
||||
const data = await this.productService.createCategory(body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分类
|
||||
@ApiOkResponse({ description: '更新分类' })
|
||||
@Put('/category/:id')
|
||||
async updateCategory(@Param('id') id: number, @Body() body: any) {
|
||||
try {
|
||||
const data = await this.productService.updateCategory(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
@ApiOkResponse({ description: '删除分类' })
|
||||
@Del('/category/:id')
|
||||
async deleteCategory(@Param('id') id: number) {
|
||||
try {
|
||||
await this.productService.deleteCategory(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建分类属性
|
||||
@ApiOkResponse({ description: '创建分类属性' })
|
||||
@Post('/category/attribute')
|
||||
async createCategoryAttribute(@Body() body: { categoryId: number; dictId: number }) {
|
||||
try {
|
||||
const data = await this.productService.createCategoryAttribute(body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分类属性
|
||||
@ApiOkResponse({ description: '删除分类属性' })
|
||||
@Del('/category/attribute/:id')
|
||||
async deleteCategoryAttribute(@Param('id') id: number) {
|
||||
try {
|
||||
await this.productService.deleteCategoryAttribute(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export class StockController {
|
|||
}
|
||||
}
|
||||
|
||||
// 中文注释:检查某个 SKU 是否有库存(任一仓库数量大于 0)
|
||||
// 检查某个 SKU 是否有库存(任一仓库数量大于 0)
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Get('/has/:sku')
|
||||
async hasStock(@Param('sku') sku: string) {
|
||||
|
|
@ -190,7 +190,7 @@ export class StockController {
|
|||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
description: '更新库存(入库、出库、调整)',
|
||||
description: '更新库存(入库,出库,调整)',
|
||||
})
|
||||
@Post('/update')
|
||||
async updateStock(@Body() body: UpdateStockDTO) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export class SubscriptionController {
|
|||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
// 同步订阅:根据站点 ID 拉取并更新本地订阅数据
|
||||
// 同步订阅:根据站点 ID 拉取并更新本地订阅数据
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Post('/sync/:siteId')
|
||||
async sync(@Param('siteId') siteId: number) {
|
||||
|
|
@ -22,7 +22,7 @@ export class SubscriptionController {
|
|||
}
|
||||
}
|
||||
|
||||
// 订阅列表:分页 + 筛选
|
||||
// 订阅列表:分页 + 筛选
|
||||
@ApiOkResponse({ type: SubscriptionListRes })
|
||||
@Get('/list')
|
||||
async list(@Query() query: QuerySubscriptionDTO) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export class TemplateController {
|
|||
|
||||
/**
|
||||
* @summary 创建新模板
|
||||
* @description 创建一个新的模板,用于后续的字符串生成
|
||||
* @description 创建一个新的模板,用于后续的字符串生成
|
||||
* @param templateData 模板数据
|
||||
*/
|
||||
@ApiOkResponse({ type: Template, description: '成功创建模板' })
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export class UserController {
|
|||
})
|
||||
@Post('/logout')
|
||||
async logout() {
|
||||
// 可选:在这里处理服务端缓存的 token 或 session
|
||||
// 可选:在这里处理服务端缓存的 token 或 session
|
||||
|
||||
return successResponse(true);
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ export class UserController {
|
|||
async addUser(@Body() body: { username: string; password: string; remark?: string }) {
|
||||
const { username, password, remark } = body;
|
||||
try {
|
||||
// 中文注释:新增用户(支持备注)
|
||||
// 新增用户(支持备注)
|
||||
await this.userService.addUser(username, password, remark);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
|
|
@ -66,9 +66,9 @@ export class UserController {
|
|||
}
|
||||
) {
|
||||
const { current = 1, pageSize = 10, remark, username, isActive, isSuper, isAdmin } = query;
|
||||
// 中文注释:将字符串布尔转换为真实布尔
|
||||
// 将字符串布尔转换为真实布尔
|
||||
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
|
||||
// 中文注释:列表移除密码字段
|
||||
// 列表移除密码字段
|
||||
const { items, total } = await this.userService.listUsers(current, pageSize, {
|
||||
remark,
|
||||
username,
|
||||
|
|
@ -86,9 +86,9 @@ export class UserController {
|
|||
@Post('/toggleActive')
|
||||
async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
|
||||
try {
|
||||
// 中文注释:调用服务层更新启用状态
|
||||
// 调用服务层更新启用状态
|
||||
const data = await this.userService.toggleUserActive(body.userId, body.isActive);
|
||||
// 中文注释:移除密码字段,保证安全
|
||||
// 移除密码字段,保证安全
|
||||
const { password, ...safe } = data as any;
|
||||
return successResponse(safe);
|
||||
} catch (error) {
|
||||
|
|
@ -96,18 +96,18 @@ export class UserController {
|
|||
}
|
||||
}
|
||||
|
||||
// 中文注释:更新用户(支持用户名/密码/权限/角色更新)
|
||||
// 更新用户(支持用户名/密码/权限/角色更新)
|
||||
@Post('/update/:id')
|
||||
async updateUser(
|
||||
@Body() body: { username?: string; password?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
|
||||
@Query('id') id?: number
|
||||
) {
|
||||
try {
|
||||
// 条件判断:优先从路径参数获取 ID(兼容生成的 API 文件为 POST /user/update/:id)
|
||||
// 条件判断:优先从路径参数获取 ID(兼容生成的 API 文件为 POST /user/update/:id)
|
||||
const userId = Number((this.ctx?.params?.id ?? id));
|
||||
if (!userId) throw new Error('缺少用户ID');
|
||||
const data = await this.userService.updateUser(userId, body);
|
||||
// 中文注释:移除密码字段,保证安全
|
||||
// 移除密码字段,保证安全
|
||||
const { password, ...safe } = data as any;
|
||||
return successResponse(safe);
|
||||
} catch (error) {
|
||||
|
|
@ -119,7 +119,7 @@ export class UserController {
|
|||
@Get()
|
||||
async getUser(@User() user) {
|
||||
try {
|
||||
// 中文注释:详情移除密码字段
|
||||
// 详情移除密码字段
|
||||
const data = await this.userService.getUser(user.id);
|
||||
const { password, ...safe } = (data as any) || {};
|
||||
return successResponse(safe);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export class WebhookController {
|
|||
@Inject()
|
||||
private readonly siteService: SiteService;
|
||||
|
||||
// 移除配置中的站点数组,来源统一改为数据库
|
||||
// 移除配置中的站点数组,来源统一改为数据库
|
||||
|
||||
@Get('/')
|
||||
async test() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from '../dto/reponse.dto';
|
||||
@Controller('/wp_product')
|
||||
export class WpProductController {
|
||||
// 移除控制器内的配置站点引用,统一由服务层处理站点数据
|
||||
// 移除控制器内的配置站点引用,统一由服务层处理站点数据
|
||||
|
||||
@Inject()
|
||||
private readonly wpProductService: WpProductService;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const options: DataSourceOptions & SeederOptions = {
|
|||
username: 'root',
|
||||
password: '12345678',
|
||||
database: 'inventory',
|
||||
synchronize: false,
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
entities: [__dirname + '/../entity/*.ts'],
|
||||
migrations: ['src/db/migrations/**/*.ts'],
|
||||
|
|
|
|||
|
|
@ -11,15 +11,20 @@ export default class AreaSeeder implements Seeder {
|
|||
const areaRepository = dataSource.getRepository(Area);
|
||||
|
||||
const areas = [
|
||||
{ name: 'Australia' },
|
||||
{ name: 'Canada' },
|
||||
{ name: 'United States' },
|
||||
{ name: 'Germany' },
|
||||
{ name: 'Poland' },
|
||||
{ name: 'Australia', code: 'AU' },
|
||||
{ name: 'Canada', code: 'CA' },
|
||||
{ name: 'United States', code: 'US' },
|
||||
{ name: 'Germany', code: 'DE' },
|
||||
{ name: 'Poland', code: 'PL' },
|
||||
];
|
||||
|
||||
for (const areaData of areas) {
|
||||
const existingArea = await areaRepository.findOne({ where: { name: areaData.name } });
|
||||
const existingArea = await areaRepository.findOne({
|
||||
where: [
|
||||
{ name: areaData.name },
|
||||
{ code: areaData.code }
|
||||
]
|
||||
});
|
||||
if (!existingArea) {
|
||||
const newArea = areaRepository.create(areaData);
|
||||
await areaRepository.save(newArea);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { Seeder } from 'typeorm-extension';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Category } from '../../entity/category.entity';
|
||||
|
||||
export default class CategorySeeder implements Seeder {
|
||||
public async run(
|
||||
dataSource: DataSource,
|
||||
): Promise<any> {
|
||||
const repository = dataSource.getRepository(Category);
|
||||
|
||||
const categories = [
|
||||
{
|
||||
name: 'nicotine-pouches',
|
||||
title: 'Nicotine Pouches',
|
||||
titleCN: '尼古丁袋',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
name: 'vape',
|
||||
title: 'vape',
|
||||
titleCN: '电子烟',
|
||||
sort: 2
|
||||
},
|
||||
{
|
||||
name: 'pouches-can',
|
||||
title: 'Pouches Can',
|
||||
titleCN: '口含烟盒',
|
||||
sort: 3
|
||||
},
|
||||
];
|
||||
|
||||
for (const cat of categories) {
|
||||
const existing = await repository.findOne({ where: { name: cat.name } });
|
||||
if (!existing) {
|
||||
await repository.save(cat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { Seeder } from 'typeorm-extension';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Dict } from '../../entity/dict.entity';
|
||||
import { Category } from '../../entity/category.entity';
|
||||
import { CategoryAttribute } from '../../entity/category_attribute.entity';
|
||||
|
||||
export default class CategoryAttributeSeeder implements Seeder {
|
||||
public async run(
|
||||
dataSource: DataSource,
|
||||
): Promise<any> {
|
||||
const dictRepository = dataSource.getRepository(Dict);
|
||||
const categoryRepository = dataSource.getRepository(Category);
|
||||
const categoryAttributeRepository = dataSource.getRepository(CategoryAttribute);
|
||||
|
||||
// 1. 确保属性字典存在
|
||||
const attributeNames = ['brand', 'strength', 'flavor', 'size', 'humidity'];
|
||||
const attributeDicts: Dict[] = [];
|
||||
|
||||
for (const name of attributeNames) {
|
||||
let dict = await dictRepository.findOne({ where: { name } });
|
||||
if (!dict) {
|
||||
dict = new Dict();
|
||||
dict.name = name;
|
||||
dict.title = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
dict.deletable = false;
|
||||
dict = await dictRepository.save(dict);
|
||||
console.log(`Created Dict: ${name}`);
|
||||
}
|
||||
attributeDicts.push(dict);
|
||||
}
|
||||
|
||||
// 2. 获取 'nicotine-pouches' 分类 (由 CategorySeeder 创建)
|
||||
const nicotinePouchesCategory = await categoryRepository.findOne({
|
||||
where: {
|
||||
name: 'nicotine-pouches'
|
||||
}
|
||||
});
|
||||
|
||||
if (!nicotinePouchesCategory) {
|
||||
console.warn('Category "nicotine-pouches" not found. Skipping attribute linking. Please ensure CategorySeeder runs first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 绑定属性到 'nicotine-pouches' 分类
|
||||
for (const attrDict of attributeDicts) {
|
||||
const existing = await categoryAttributeRepository.findOne({
|
||||
where: {
|
||||
category: { id: nicotinePouchesCategory.id },
|
||||
attributeDict: { id: attrDict.id }
|
||||
}
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const link = new CategoryAttribute();
|
||||
link.category = nicotinePouchesCategory;
|
||||
link.attributeDict = attrDict;
|
||||
await categoryAttributeRepository.save(link);
|
||||
console.log(`Linked ${attrDict.name} to ${nicotinePouchesCategory.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,79 +22,77 @@ export default class DictSeeder implements Seeder {
|
|||
const dictItemRepository = dataSource.getRepository(DictItem);
|
||||
|
||||
const flavorsData = [
|
||||
{ title: 'Bellini', name: 'bellini' },
|
||||
{ title: 'Max Polarmint', name: 'max-polarmint' },
|
||||
{ title: 'Blueberry', name: 'blueberry' },
|
||||
{ title: 'Citrus', name: 'citrus' },
|
||||
{ title: 'Wintergreen', name: 'wintergreen' },
|
||||
{ title: 'COOL MINT', name: 'cool-mint' },
|
||||
{ title: 'JUICY PEACH', name: 'juicy-peach' },
|
||||
{ title: 'ORANGE', name: 'orange' },
|
||||
{ title: 'PEPPERMINT', name: 'peppermint' },
|
||||
{ title: 'SPEARMINT', name: 'spearmint' },
|
||||
{ title: 'STRAWBERRY', name: 'strawberry' },
|
||||
{ title: 'WATERMELON', name: 'watermelon' },
|
||||
{ title: 'COFFEE', name: 'coffee' },
|
||||
{ title: 'LEMONADE', name: 'lemonade' },
|
||||
{ title: 'apple mint', name: 'apple-mint' },
|
||||
{ title: 'PEACH', name: 'peach' },
|
||||
{ title: 'Mango', name: 'mango' },
|
||||
{ title: 'ICE WINTERGREEN', name: 'ice-wintergreen' },
|
||||
{ title: 'Pink Lemonade', name: 'pink-lemonade' },
|
||||
{ title: 'Blackcherry', name: 'blackcherry' },
|
||||
{ title: 'fresh mint', name: 'fresh-mint' },
|
||||
{ title: 'Strawberry Lychee', name: 'strawberry-lychee' },
|
||||
{ title: 'Passion Fruit', name: 'passion-fruit' },
|
||||
{ title: 'Banana lce', name: 'banana-lce' },
|
||||
{ title: 'Bubblegum', name: 'bubblegum' },
|
||||
{ title: 'Mango lce', name: 'mango-lce' },
|
||||
{ title: 'Grape lce', name: 'grape-lce' },
|
||||
{ title: 'apple', name: 'apple' },
|
||||
{ title: 'grape', name: 'grape' },
|
||||
{ title: 'cherry', name: 'cherry' },
|
||||
{ title: 'lemon', name: 'lemon' },
|
||||
{ title: 'razz', name: 'razz' },
|
||||
{ title: 'pineapple', name: 'pineapple' },
|
||||
{ title: 'berry', name: 'berry' },
|
||||
{ title: 'fruit', name: 'fruit' },
|
||||
{ title: 'mint', name: 'mint' },
|
||||
{ title: 'menthol', name: 'menthol' },
|
||||
{ name: 'bellini', title: 'Bellini', titleCn: '贝利尼' },
|
||||
{ name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷' },
|
||||
{ name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓' },
|
||||
{ name: 'citrus', title: 'Citrus', titleCn: '柑橘' },
|
||||
{ name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷' },
|
||||
{ name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷' },
|
||||
{ name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃' },
|
||||
{ name: 'orange', title: 'ORANGE', titleCn: '橙子' },
|
||||
{ name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷' },
|
||||
{ name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷' },
|
||||
{ name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓' },
|
||||
{ name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜' },
|
||||
{ name: 'coffee', title: 'COFFEE', titleCn: '咖啡' },
|
||||
{ name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水' },
|
||||
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷' },
|
||||
{ name: 'peach', title: 'PEACH', titleCn: '桃子' },
|
||||
{ name: 'mango', title: 'Mango', titleCn: '芒果' },
|
||||
{ name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷' },
|
||||
{ name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水' },
|
||||
{ name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃' },
|
||||
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷' },
|
||||
{ name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝' },
|
||||
{ name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果' },
|
||||
{ name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰' },
|
||||
{ name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖' },
|
||||
{ name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰' },
|
||||
{ name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰' },
|
||||
{ name: 'apple', title: 'apple', titleCn: '苹果' },
|
||||
{ name: 'grape', title: 'grape', titleCn: '葡萄' },
|
||||
{ name: 'cherry', title: 'cherry', titleCn: '樱桃' },
|
||||
{ name: 'lemon', title: 'lemon', titleCn: '柠檬' },
|
||||
{ name: 'razz', title: 'razz', titleCn: '覆盆子' },
|
||||
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝' },
|
||||
{ name: 'berry', title: 'berry', titleCn: '浆果' },
|
||||
{ name: 'fruit', title: 'fruit', titleCn: '水果' },
|
||||
{ name: 'mint', title: 'mint', titleCn: '薄荷' },
|
||||
{ name: 'menthol', title: 'menthol', titleCn: '薄荷醇' },
|
||||
];
|
||||
|
||||
const brandsData = [
|
||||
{ title: 'Yoone', name: 'yoone' },
|
||||
{ title: 'White Fox', name: 'white-fox' },
|
||||
{ title: 'ZYN', name: 'zyn' },
|
||||
{ title: 'Zonnic', name: 'zonnic' },
|
||||
{ title: 'Zolt', name: 'zolt' },
|
||||
{ title: 'Velo', name: 'velo' },
|
||||
{ title: 'Lucy', name: 'lucy' },
|
||||
{ title: 'EGP', name: 'egp' },
|
||||
{ title: 'Bridge', name: 'bridge' },
|
||||
{ title: 'ZEX', name: 'zex' },
|
||||
{ title: 'Sesh', name: 'sesh' },
|
||||
{ title: 'Pablo', name: 'pablo' },
|
||||
{ name: 'yoone', title: 'Yoone', titleCn: '' },
|
||||
{ name: 'white-fox', title: 'White Fox', titleCn: '' },
|
||||
{ name: 'zyn', title: 'ZYN', titleCn: '' },
|
||||
{ name: 'zonnic', title: 'Zonnic', titleCn: '' },
|
||||
{ name: 'zolt', title: 'Zolt', titleCn: '' },
|
||||
{ name: 'velo', title: 'Velo', titleCn: '' },
|
||||
{ name: 'lucy', title: 'Lucy', titleCn: '' },
|
||||
{ name: 'egp', title: 'EGP', titleCn: '' },
|
||||
{ name: 'bridge', title: 'Bridge', titleCn: '' },
|
||||
{ name: 'zex', title: 'ZEX', titleCn: '' },
|
||||
{ name: 'sesh', title: 'Sesh', titleCn: '' },
|
||||
{ name: 'pablo', title: 'Pablo', titleCn: '' },
|
||||
];
|
||||
|
||||
const strengthsData = [
|
||||
{ title: '3MG', name: '3mg' },
|
||||
{ title: '9MG', name: '9mg' },
|
||||
{ title: '2MG', name: '2mg' },
|
||||
{ title: '4MG', name: '4mg' },
|
||||
{ title: '12MG', name: '12mg' },
|
||||
{ title: '18MG', name: '18mg' },
|
||||
{ title: '6MG', name: '6mg' },
|
||||
{ title: '16.5MG', name: '16-5mg' },
|
||||
{ title: '6.5MG', name: '6-5mg' },
|
||||
{ title: '30MG', name: '30mg' },
|
||||
{ name: '2mg', title: '2MG', titleCn: '2毫克' },
|
||||
{ name: '4mg', title: '4MG', titleCn: '4毫克' },
|
||||
{ name: '3mg', title: '3MG', titleCn: '3毫克' },
|
||||
{ name: '6mg', title: '6MG', titleCn: '6毫克' },
|
||||
{ name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克' },
|
||||
{ name: '9mg', title: '9MG', titleCn: '9毫克' },
|
||||
{ name: '12mg', title: '12MG', titleCn: '12毫克' },
|
||||
{ name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克' },
|
||||
{ name: '18mg', title: '18MG', titleCn: '18毫克' },
|
||||
{ name: '30mg', title: '30MG', titleCn: '30毫克' },
|
||||
];
|
||||
|
||||
const nonFlavorTokensData = ['slim', 'pouches', 'pouch', 'mini', 'dry'].map(item => ({ title: item, name: item }));
|
||||
|
||||
// 初始化语言字典
|
||||
const locales = [
|
||||
{ name: 'zh-cn', title: '简体中文' },
|
||||
{ name: 'en-us', title: 'English' },
|
||||
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文' },
|
||||
{ name: 'en-us', title: 'English', titleCn: '英文' },
|
||||
];
|
||||
|
||||
for (const locale of locales) {
|
||||
|
|
@ -116,20 +114,19 @@ export default class DictSeeder implements Seeder {
|
|||
// 添加中文翻译
|
||||
let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
|
||||
if (!item) {
|
||||
await dictItemRepository.save({ name: t.name, title: t.zh, dict: zhDict });
|
||||
await dictItemRepository.save({ name: t.name, title: t.zh, titleCn: t.zh, dict: zhDict });
|
||||
}
|
||||
|
||||
// 添加英文翻译
|
||||
item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
|
||||
if (!item) {
|
||||
await dictItemRepository.save({ name: t.name, title: t.en, dict: enDict });
|
||||
await dictItemRepository.save({ name: t.name, title: t.en, titleCn: t.en, dict: enDict });
|
||||
}
|
||||
}
|
||||
|
||||
const brandDict = await this.createOrFindDict(dictRepository, { title: '品牌', name: 'brand' });
|
||||
const flavorDict = await this.createOrFindDict(dictRepository, { title: '口味', name: 'flavor' });
|
||||
const strengthDict = await this.createOrFindDict(dictRepository, { title: '强度', name: 'strength' });
|
||||
const nonFlavorTokensDict = await this.createOrFindDict(dictRepository, { title: '非口味关键词', name: 'non-flavor-tokens' });
|
||||
const brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌' });
|
||||
const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味' });
|
||||
const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度' });
|
||||
|
||||
// 遍历品牌数据
|
||||
await this.seedDictItems(dictItemRepository, brandDict, brandsData);
|
||||
|
|
@ -139,9 +136,6 @@ export default class DictSeeder implements Seeder {
|
|||
|
||||
// 遍历强度数据
|
||||
await this.seedDictItems(dictItemRepository, strengthDict, strengthsData);
|
||||
|
||||
// 遍历非口味关键词数据
|
||||
await this.seedDictItems(dictItemRepository, nonFlavorTokensDict, nonFlavorTokensData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -150,13 +144,13 @@ export default class DictSeeder implements Seeder {
|
|||
* @param dictInfo 字典信息
|
||||
* @returns Dict 实例
|
||||
*/
|
||||
private async createOrFindDict(repo: any, dictInfo: { title: string; name: string }): Promise<Dict> {
|
||||
private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string }): Promise<Dict> {
|
||||
// 格式化 name
|
||||
const formattedName = this.formatName(dictInfo.name);
|
||||
let dict = await repo.findOne({ where: { name: formattedName } });
|
||||
if (!dict) {
|
||||
// 如果字典不存在,则使用格式化后的 name 创建新字典
|
||||
dict = await repo.save({ title: dictInfo.title, name: formattedName });
|
||||
// 如果字典不存在,则使用格式化后的 name 创建新字典
|
||||
dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn });
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
|
@ -167,14 +161,14 @@ export default class DictSeeder implements Seeder {
|
|||
* @param dict 字典实例
|
||||
* @param items 字典项数组
|
||||
*/
|
||||
private async seedDictItems(repo: any, dict: Dict, items: { title: string; name: string }[]): Promise<void> {
|
||||
private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string }[]): Promise<void> {
|
||||
for (const item of items) {
|
||||
// 格式化 name
|
||||
const formattedName = this.formatName(item.name);
|
||||
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
|
||||
if (!existingItem) {
|
||||
// 如果字典项不存在,则使用格式化后的 name 创建新字典项
|
||||
await repo.save({ ...item, name: formattedName, dict });
|
||||
// 如果字典项不存在,则使用格式化后的 name 创建新字典项
|
||||
await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, dict });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import { Template } from '../../entity/template.entity';
|
|||
|
||||
/**
|
||||
* @class TemplateSeeder
|
||||
* @description 模板数据填充器,用于在数据库初始化时插入默认的模板数据。
|
||||
* @description 模板数据填充器,用于在数据库初始化时插入默认的模板数据.
|
||||
*/
|
||||
export default class TemplateSeeder implements Seeder {
|
||||
/**
|
||||
* @method run
|
||||
* @description 执行数据填充操作。如果 product_sku 模板不存在,则创建它。
|
||||
* @param {DataSource} dataSource - 数据源实例,用于获取 repository。
|
||||
* @param {SeederFactoryManager} factoryManager - Seeder 工厂管理器。
|
||||
* @description 执行数据填充操作.如果模板不存在,则创建它;如果存在,则更新它.
|
||||
* @param {DataSource} dataSource - 数据源实例,用于获取 repository.
|
||||
* @param {SeederFactoryManager} factoryManager - Seeder 工厂管理器.
|
||||
*/
|
||||
public async run(
|
||||
dataSource: DataSource,
|
||||
|
|
@ -20,17 +20,38 @@ export default class TemplateSeeder implements Seeder {
|
|||
// 获取 Template 实体的 repository
|
||||
const templateRepository = dataSource.getRepository(Template);
|
||||
|
||||
// 检查名为 'product_sku' 的模板是否已存在
|
||||
const templates = [
|
||||
{
|
||||
name: 'product.sku',
|
||||
value: '<%= it.brand %>-<%=it.category%>-<%= it.flavor %>-<%= it.strength %>-<%= it.humidity %>',
|
||||
description: '产品SKU模板',
|
||||
},
|
||||
{
|
||||
name: 'product.title',
|
||||
value: '<%= it.brand %> <%= it.flavor %> <%= it.strength %> <%= it.humidity %>',
|
||||
description: '产品标题模板',
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of templates) {
|
||||
// 检查模板是否已存在
|
||||
const existingTemplate = await templateRepository.findOne({
|
||||
where: { name: 'product_sku' },
|
||||
where: { name: t.name },
|
||||
});
|
||||
|
||||
// 如果模板不存在,则创建并保存
|
||||
if (!existingTemplate) {
|
||||
if (existingTemplate) {
|
||||
// 如果存在,则更新
|
||||
existingTemplate.value = t.value;
|
||||
existingTemplate.description = t.description;
|
||||
await templateRepository.save(existingTemplate);
|
||||
} else {
|
||||
// 如果不存在,则创建并保存
|
||||
const template = new Template();
|
||||
template.name = 'product_sku';
|
||||
template.value = '{{brand}}-{{flavor}}-{{strength}}-{{humidity}}';
|
||||
template.name = t.name;
|
||||
template.value = t.value;
|
||||
template.description = t.description;
|
||||
await templateRepository.save(template);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class QueryAreaDTO {
|
|||
@Rule(RuleType.number().integer().min(1).default(10))
|
||||
pageSize?: number;
|
||||
|
||||
@ApiProperty({ description: '关键词(名称或编码)', required: false })
|
||||
@ApiProperty({ description: '关键词(名称或编码)', required: false })
|
||||
@Rule(RuleType.string())
|
||||
keyword?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export type PackagingType =
|
|||
// | PackagingCourierPak
|
||||
// | PackagingEnvelope;
|
||||
|
||||
// 定义包装类型的枚举,用于 API 文档描述
|
||||
// 定义包装类型的枚举,用于 API 文档描述
|
||||
export enum PackagingTypeEnum {
|
||||
Pallet = 'pallet',
|
||||
Package = 'package',
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export class QueryOrderDTO {
|
|||
@Rule(RuleType.string())
|
||||
payment_method: string;
|
||||
|
||||
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
|
||||
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
|
||||
@Rule(RuleType.bool().default(false))
|
||||
isSubscriptionOnly?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,31 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Rule, RuleType } from '@midwayjs/validate';
|
||||
|
||||
/**
|
||||
* 属性输入DTO
|
||||
*/
|
||||
export class AttributeInputDTO {
|
||||
@ApiProperty({ description: '属性字典标识', example: 'brand' })
|
||||
@Rule(RuleType.string())
|
||||
dictName?: string;
|
||||
|
||||
@ApiProperty({ description: '属性值', example: 'ZYN' })
|
||||
@Rule(RuleType.string())
|
||||
value?: string;
|
||||
|
||||
@ApiProperty({ description: '属性ID', example: 1 })
|
||||
@Rule(RuleType.number())
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ description: '属性名称', example: 'ZYN' })
|
||||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: '属性显示名称', example: 'ZYN' })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建产品
|
||||
*/
|
||||
|
|
@ -13,6 +38,10 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.string().required().empty({ message: '产品名称不能为空' }))
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '产品中文名称', required: false })
|
||||
@Rule(RuleType.string().allow('').optional())
|
||||
nameCn?: string;
|
||||
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||
@Rule(RuleType.string())
|
||||
description: string;
|
||||
|
|
@ -21,7 +50,11 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.string())
|
||||
sku?: string;
|
||||
|
||||
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
|
||||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||
@ApiProperty({ description: '属性列表', type: 'array' })
|
||||
@Rule(RuleType.array().required())
|
||||
attributes: AttributeInputDTO[];
|
||||
|
|
@ -36,12 +69,14 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
// 中文注释:商品类型(默认 simple;bundle 需手动设置组成)
|
||||
@ApiProperty({ description: '商品类型', enum: ['simple', 'bundle'], default: 'simple', required: false })
|
||||
@Rule(RuleType.string().valid('simple', 'bundle').default('simple'))
|
||||
|
||||
|
||||
// 商品类型(默认 single; bundle 需手动设置组成)
|
||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
|
||||
@Rule(RuleType.string().valid('single', 'bundle').default('single'))
|
||||
type?: string;
|
||||
|
||||
// 中文注释:仅当 type 为 'bundle' 时,才需要提供 components
|
||||
// 仅当 type 为 'bundle' 时,才需要提供 components
|
||||
@ApiProperty({ description: '产品组成', type: 'array', required: false })
|
||||
@Rule(
|
||||
RuleType.array()
|
||||
|
|
@ -67,6 +102,10 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: '产品中文名称', required: false })
|
||||
@Rule(RuleType.string().allow('').optional())
|
||||
nameCn?: string;
|
||||
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||
@Rule(RuleType.string())
|
||||
description?: string;
|
||||
|
|
@ -75,6 +114,10 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.string())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
|
||||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number())
|
||||
|
|
@ -85,234 +128,125 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
// 属性更新(中文注释:可选,支持增量替换指定字典的属性项)
|
||||
|
||||
|
||||
// 属性更新(可选, 支持增量替换指定字典的属性项)
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array())
|
||||
attributes?: AttributeInputDTO[];
|
||||
|
||||
// 中文注释:商品类型更新(simple 或 bundle)
|
||||
@ApiProperty({ description: '商品类型', enum: ['simple', 'bundle'], required: false })
|
||||
@Rule(RuleType.string().valid('simple', 'bundle'))
|
||||
// 商品类型(single 或 bundle)
|
||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
||||
@Rule(RuleType.string().valid('single', 'bundle'))
|
||||
type?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DTO 用于批量更新产品属性
|
||||
*/
|
||||
export class BatchUpdateProductDTO {
|
||||
@ApiProperty({ description: '产品ID列表', type: 'array', required: true })
|
||||
@Rule(RuleType.array().items(RuleType.number()).required().min(1))
|
||||
ids: number[];
|
||||
|
||||
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: '产品中文名称', required: false })
|
||||
@Rule(RuleType.string().allow('').optional())
|
||||
nameCn?: string;
|
||||
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: '产品 SKU', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
|
||||
@Rule(RuleType.number().optional())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number().optional())
|
||||
price?: number;
|
||||
|
||||
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number().optional())
|
||||
promotionPrice?: number;
|
||||
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
attributes?: AttributeInputDTO[];
|
||||
|
||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
||||
@Rule(RuleType.string().valid('single', 'bundle').optional())
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建分类属性绑定
|
||||
*/
|
||||
export class CreateCategoryAttributeDTO {
|
||||
@ApiProperty({ description: '分类字典项ID', example: 1 })
|
||||
@Rule(RuleType.number().required())
|
||||
categoryItemId: number;
|
||||
|
||||
@ApiProperty({ description: '属性字典ID列表', example: [2, 3] })
|
||||
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||
attributeDictIds: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于分页查询产品
|
||||
*/
|
||||
export class QueryProductDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
@ApiProperty({ description: '当前页', example: 1 })
|
||||
@Rule(RuleType.number().default(1))
|
||||
current: number;
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
@ApiProperty({ description: '每页数量', example: 10 })
|
||||
@Rule(RuleType.number().default(10))
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: '1', description: '品牌 ID' })
|
||||
@Rule(RuleType.string())
|
||||
brandId: number;
|
||||
}
|
||||
|
||||
// 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息)
|
||||
export class AttributeInputDTO {
|
||||
@ApiProperty({ description: '字典名称', example: 'brand', required: false})
|
||||
@Rule(RuleType.string())
|
||||
dictName?: string;
|
||||
|
||||
@ApiProperty({ description: '字典项 ID', required: false })
|
||||
@Rule(RuleType.number())
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ description: '字典项显示名称', required: false })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ description: '字典项唯一标识', required: false })
|
||||
@ApiProperty({ description: '搜索关键字', required: false })
|
||||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: '分类ID', required: false })
|
||||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '品牌ID', required: false })
|
||||
@Rule(RuleType.number())
|
||||
brandId?: number;
|
||||
|
||||
@ApiProperty({ description: '排序字段', required: false })
|
||||
@Rule(RuleType.string())
|
||||
sortField?: string;
|
||||
|
||||
@ApiProperty({ description: '排序方式', required: false })
|
||||
@Rule(RuleType.string().valid('ascend', 'descend'))
|
||||
sortOrder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建品牌
|
||||
* DTO 用于设置产品组成
|
||||
*/
|
||||
export class CreateBrandDTO {
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌名称', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: '品牌名称不能为空' }))
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌唯一标识', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于更新品牌
|
||||
*/
|
||||
export class UpdateBrandDTO {
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于查询品牌(支持分页)
|
||||
*/
|
||||
export class QueryBrandDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
export class CreateFlavorsDTO {
|
||||
@ApiProperty({ example: 'WINTERGREEN', description: '口味名称', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: '口味名称不能为空' }))
|
||||
title: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'WINTERGREEN',
|
||||
description: '口味唯一标识',
|
||||
required: true,
|
||||
})
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateFlavorsDTO {
|
||||
@ApiProperty({ example: 'WINTERGREEN', description: '口味名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'WINTERGREEN', description: '口味唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class QueryFlavorsDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
export class CreateStrengthDTO {
|
||||
@ApiProperty({ example: '6MG', description: '规格名称', required: false })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ example: '6MG', description: '规格唯一标识', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateStrengthDTO {
|
||||
@ApiProperty({ example: '6MG', description: '规格名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: '6MG', description: '规格唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class QueryStrengthDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: 'YOONE', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
// size 新增 DTO
|
||||
export class CreateSizeDTO {
|
||||
@ApiProperty({ example: '6', description: '尺寸名称', required: false })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ example: '6', description: '尺寸唯一标识', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateSizeDTO {
|
||||
@ApiProperty({ example: '6', description: '尺寸名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: '6', description: '尺寸唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class QuerySizeDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: '6', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
export class SkuItemDTO {
|
||||
@ApiProperty({ description: '产品 ID' })
|
||||
productId: number;
|
||||
|
||||
@ApiProperty({ description: 'sku 编码' })
|
||||
sku: string;
|
||||
}
|
||||
|
||||
export class BatchSetSkuDTO {
|
||||
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
|
||||
skus: SkuItemDTO[];
|
||||
}
|
||||
|
||||
// 中文注释:产品库存组成项输入
|
||||
export class ProductComponentItemDTO {
|
||||
@ApiProperty({ description: '组件 SKU' })
|
||||
@Rule(RuleType.string().required())
|
||||
sku: string;
|
||||
|
||||
@ApiProperty({ description: '组成数量', example: 1 })
|
||||
@Rule(RuleType.number().min(1).default(1))
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
// 中文注释:设置产品库存组成输入
|
||||
export class SetProductComponentsDTO {
|
||||
@ApiProperty({ description: '组成项列表', type: [ProductComponentItemDTO] })
|
||||
@Rule(RuleType.array().items(RuleType.object()))
|
||||
items: ProductComponentItemDTO[];
|
||||
@ApiProperty({ description: '产品组成', type: 'array', required: true })
|
||||
@Rule(
|
||||
RuleType.array()
|
||||
.items(
|
||||
RuleType.object({
|
||||
sku: RuleType.string().required(),
|
||||
quantity: RuleType.number().required(),
|
||||
})
|
||||
)
|
||||
.required()
|
||||
)
|
||||
components: { sku: string; quantity: number }[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export class PaymentMethodListRes extends SuccessArrayWrapper(
|
|||
PaymentMethodDTO
|
||||
) {}
|
||||
|
||||
// 订阅分页数据(列表 + 总数等分页信息)
|
||||
// 订阅分页数据(列表 + 总数等分页信息)
|
||||
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
|
||||
// 订阅分页返回数据(统一成功包装)
|
||||
// 订阅分页返回数据(统一成功包装)
|
||||
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}
|
||||
|
|
|
|||
|
|
@ -38,12 +38,19 @@ export class CreateSiteDTO {
|
|||
consumerKey?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
consumerSecret?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
token?: string;
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||
type?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
}
|
||||
|
||||
export class UpdateSiteDTO {
|
||||
|
|
@ -54,6 +61,8 @@ export class UpdateSiteDTO {
|
|||
@Rule(RuleType.string().optional())
|
||||
consumerSecret?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
token?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
name?: string;
|
||||
@Rule(RuleType.boolean().optional())
|
||||
isDisabled?: boolean;
|
||||
|
|
@ -61,6 +70,11 @@ export class UpdateSiteDTO {
|
|||
type?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
}
|
||||
|
||||
export class QuerySiteDTO {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class QueryStockDTO {
|
|||
@Rule(RuleType.number().allow(null))
|
||||
sortPointId?: number;
|
||||
|
||||
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }', required: false })
|
||||
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }', required: false })
|
||||
@Rule(RuleType.object().allow(null))
|
||||
order?: Record<string, 'asc' | 'desc'>;
|
||||
}
|
||||
|
|
@ -167,6 +167,11 @@ export class CreateStockPointDTO {
|
|||
@ApiProperty()
|
||||
@Rule(RuleType.string())
|
||||
contactPhone: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
}
|
||||
export class UpdateStockPointDTO extends CreateStockPointDTO {}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { ApiProperty } from '@midwayjs/swagger';
|
|||
import { Rule, RuleType } from '@midwayjs/validate';
|
||||
import { SubscriptionStatus } from '../enums/base.enum';
|
||||
|
||||
// 订阅列表查询参数(分页与筛选)
|
||||
// 订阅列表查询参数(分页与筛选)
|
||||
export class QuerySubscriptionDTO {
|
||||
// 当前页码(从 1 开始)
|
||||
// 当前页码(从 1 开始)
|
||||
@ApiProperty({ example: 1, description: '页码' })
|
||||
@Rule(RuleType.number().default(1))
|
||||
current: number;
|
||||
|
|
@ -14,23 +14,23 @@ export class QuerySubscriptionDTO {
|
|||
@Rule(RuleType.number().default(10))
|
||||
pageSize: number;
|
||||
|
||||
// 站点 ID(可选)
|
||||
// 站点 ID(可选)
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Rule(RuleType.string().allow(''))
|
||||
siteId: string;
|
||||
|
||||
// 订阅状态筛选(可选),支持枚举值
|
||||
// 订阅状态筛选(可选),支持枚举值
|
||||
@ApiProperty({ description: '订阅状态', enum: SubscriptionStatus })
|
||||
@Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow(''))
|
||||
status: SubscriptionStatus | '';
|
||||
|
||||
// 客户邮箱(模糊匹配,可选)
|
||||
// 客户邮箱(模糊匹配,可选)
|
||||
@ApiProperty({ description: '客户邮箱' })
|
||||
@Rule(RuleType.string().allow(''))
|
||||
customer_email: string;
|
||||
|
||||
// 关键字(订阅ID、邮箱等,模糊匹配,可选)
|
||||
@ApiProperty({ description: '关键字(订阅ID、邮箱等)' })
|
||||
// 关键字(订阅ID,邮箱等,模糊匹配,可选)
|
||||
@ApiProperty({ description: '关键字(订阅ID,邮箱等)' })
|
||||
@Rule(RuleType.string().allow(''))
|
||||
keyword: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Product } from './product.entity';
|
||||
import { CategoryAttribute } from './category_attribute.entity';
|
||||
|
||||
@Entity('category')
|
||||
export class Category {
|
||||
@ApiProperty({ description: 'ID' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '分类显示名称' })
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: '分类中文名称' })
|
||||
@Column({ nullable: true })
|
||||
titleCN: string;
|
||||
|
||||
@ApiProperty({ description: '分类唯一标识' })
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '排序' })
|
||||
@Column({ default: 0 })
|
||||
sort: number;
|
||||
|
||||
@OneToMany(() => Product, product => product.category)
|
||||
products: Product[];
|
||||
|
||||
@OneToMany(() => CategoryAttribute, categoryAttribute => categoryAttribute.category)
|
||||
attributes: CategoryAttribute[];
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Category } from './category.entity';
|
||||
import { Dict } from './dict.entity';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
|
||||
@Entity()
|
||||
export class CategoryAttribute {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '分类' })
|
||||
@ManyToOne(() => Category, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'category_id' })
|
||||
category: Category;
|
||||
|
||||
@ApiProperty({ description: '关联的属性字典' })
|
||||
@ManyToOne(() => Dict, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'attribute_dict_id' })
|
||||
attributeDict: Dict;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ export class Order {
|
|||
@ApiProperty()
|
||||
@Column({
|
||||
type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT
|
||||
nullable: true, // 可选:是否允许为 NULL
|
||||
nullable: true, // 可选:是否允许为 NULL
|
||||
})
|
||||
@Expose()
|
||||
customer_note: string;
|
||||
|
|
|
|||
|
|
@ -79,17 +79,17 @@ export class OrderItem {
|
|||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
tax_class?: string; // 税类(来自 line_items.tax_class)
|
||||
tax_class?: string; // 税类(来自 line_items.tax_class)
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
taxes?: any[]; // 税明细(来自 line_items.taxes,数组)
|
||||
taxes?: any[]; // 税明细(来自 line_items.taxes,数组)
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
|
||||
meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
|
|
@ -99,7 +99,7 @@ export class OrderItem {
|
|||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供)
|
||||
global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供)
|
||||
|
||||
@ApiProperty()
|
||||
@Column('decimal', { precision: 10, scale: 2 })
|
||||
|
|
@ -109,17 +109,17 @@ export class OrderItem {
|
|||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src)
|
||||
image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src)
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
|
||||
parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
bundled_by?: string; // 捆绑来源标识(bundled_by)
|
||||
bundled_by?: string; // 捆绑来源标识(bundled_by)
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
|
|
@ -129,7 +129,7 @@ export class OrderItem {
|
|||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
bundled_items?: any[]; // 捆绑项列表(数组)
|
||||
bundled_items?: any[]; // 捆绑项列表(数组)
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ import {
|
|||
ManyToMany,
|
||||
JoinTable,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { DictItem } from './dict_item.entity';
|
||||
import { ProductStockComponent } from './product_stock_component.entity';
|
||||
import { Category } from './category.entity';
|
||||
|
||||
@Entity()
|
||||
export class Product {
|
||||
|
|
@ -60,9 +63,13 @@ export class Product {
|
|||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
promotionPrice: number;
|
||||
|
||||
@ApiProperty({ description: '库存', example: 100 })
|
||||
@Column({ default: 0 })
|
||||
stock: number;
|
||||
|
||||
|
||||
|
||||
// 分类关联
|
||||
@ManyToOne(() => Category, category => category.products)
|
||||
@JoinColumn({ name: 'categoryId' })
|
||||
category: Category;
|
||||
|
||||
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
||||
cascade: true,
|
||||
|
|
@ -70,7 +77,7 @@ export class Product {
|
|||
@JoinTable()
|
||||
attributes: DictItem[];
|
||||
|
||||
// 中文注释:产品的库存组成,一对多关系(使用独立表)
|
||||
// 产品的库存组成,一对多关系(使用独立表)
|
||||
@ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
|
||||
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
||||
components: ProductStockComponent[];
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export class ProductStockComponent {
|
|||
@Column({ type: 'int', default: 1 })
|
||||
quantity: number;
|
||||
|
||||
// 中文注释:多对一,组件隶属于一个产品
|
||||
// 多对一,组件隶属于一个产品
|
||||
@ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
|
||||
product: Product;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class Site {
|
|||
name: string;
|
||||
|
||||
@Column({ length: 32, default: 'woocommerce' })
|
||||
type: string; // 平台类型:woocommerce | shopyy
|
||||
type: string; // 平台类型:woocommerce | shopyy
|
||||
|
||||
@Column({ length: 64, nullable: true })
|
||||
skuPrefix: string;
|
||||
|
|
|
|||
|
|
@ -12,68 +12,68 @@ import { SubscriptionStatus } from '../enums/base.enum';
|
|||
@Entity('subscription')
|
||||
@Exclude()
|
||||
export class Subscription {
|
||||
// 本地主键,自增 ID
|
||||
// 本地主键,自增 ID
|
||||
@ApiProperty()
|
||||
@PrimaryGeneratedColumn()
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
// 站点唯一标识,用于区分不同来源站点
|
||||
// 站点唯一标识,用于区分不同来源站点
|
||||
@ApiProperty({ description: '来源站点唯一标识' })
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
siteId: number;
|
||||
|
||||
// WooCommerce 订阅的原始 ID(字符串化),用于幂等更新
|
||||
// WooCommerce 订阅的原始 ID(字符串化),用于幂等更新
|
||||
@ApiProperty({ description: 'WooCommerce 订阅 ID' })
|
||||
@Column()
|
||||
@Expose()
|
||||
externalSubscriptionId: string;
|
||||
|
||||
// 订阅状态(active/cancelled/on-hold 等)
|
||||
// 订阅状态(active/cancelled/on-hold 等)
|
||||
@ApiProperty({ type: SubscriptionStatus })
|
||||
@Column({ type: 'enum', enum: SubscriptionStatus })
|
||||
@Expose()
|
||||
status: SubscriptionStatus;
|
||||
|
||||
// 货币代码,例如 USD/CAD
|
||||
// 货币代码,例如 USD/CAD
|
||||
@ApiProperty()
|
||||
@Column({ default: '' })
|
||||
@Expose()
|
||||
currency: string;
|
||||
|
||||
// 总金额,保留两位小数
|
||||
// 总金额,保留两位小数
|
||||
@ApiProperty()
|
||||
@Column('decimal', { precision: 10, scale: 2, default: 0 })
|
||||
@Expose()
|
||||
total: number;
|
||||
|
||||
// 计费周期(day/week/month/year)
|
||||
// 计费周期(day/week/month/year)
|
||||
@ApiProperty({ description: '计费周期 e.g. day/week/month/year' })
|
||||
@Column({ default: '' })
|
||||
@Expose()
|
||||
billing_period: string;
|
||||
|
||||
// 计费周期间隔(例如 1/3/12)
|
||||
// 计费周期间隔(例如 1/3/12)
|
||||
@ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' })
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Expose()
|
||||
billing_interval: number;
|
||||
|
||||
// 客户 ID(WooCommerce 用户 ID)
|
||||
// 客户 ID(WooCommerce 用户 ID)
|
||||
@ApiProperty()
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Expose()
|
||||
customer_id: number;
|
||||
|
||||
// 客户邮箱(从 billing.email 或 customer_email 提取)
|
||||
// 客户邮箱(从 billing.email 或 customer_email 提取)
|
||||
@ApiProperty()
|
||||
@Column({ default: '' })
|
||||
@Expose()
|
||||
customer_email: string;
|
||||
|
||||
// 父订单/订阅 ID(如有)
|
||||
@ApiProperty({ description: '父订单/父订阅ID(如有)' })
|
||||
// 父订单/订阅 ID(如有)
|
||||
@ApiProperty({ description: '父订单/父订阅ID(如有)' })
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Expose()
|
||||
parent_id: number;
|
||||
|
|
@ -102,25 +102,25 @@ export class Subscription {
|
|||
@Expose()
|
||||
end_date: Date;
|
||||
|
||||
// 商品项(订阅行项目)
|
||||
// 商品项(订阅行项目)
|
||||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
line_items: any[];
|
||||
|
||||
// 额外元数据(键值对)
|
||||
// 额外元数据(键值对)
|
||||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
meta_data: any[];
|
||||
|
||||
// 创建时间(数据库自动生成)
|
||||
// 创建时间(数据库自动生成)
|
||||
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
|
||||
@CreateDateColumn()
|
||||
@Expose()
|
||||
createdAt: Date;
|
||||
|
||||
// 更新时间(数据库自动生成)
|
||||
// 更新时间(数据库自动生成)
|
||||
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
|
||||
@UpdateDateColumn()
|
||||
@Expose()
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ export class User {
|
|||
password: string;
|
||||
|
||||
// @Column() // 默认角色为管理员
|
||||
// roleId: number; // 角色 (如:admin, editor, viewer)
|
||||
// roleId: number; // 角色 (如:admin, editor, viewer)
|
||||
|
||||
@Column({ type: 'simple-array', nullable: true })
|
||||
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
||||
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
||||
|
||||
@Column({ default: false })
|
||||
isSuper: boolean; // 超级管理员
|
||||
|
|
@ -29,7 +29,7 @@ export class User {
|
|||
@Column({ default: true })
|
||||
isActive: boolean; // 用户是否启用
|
||||
|
||||
// 中文注释:备注字段(可选)
|
||||
// 备注字段(可选)
|
||||
@Column({ nullable: true })
|
||||
remark?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
|
|||
return async (ctx: Context, next: NextFunction) => {
|
||||
// 控制器前执行的逻辑
|
||||
const startTime = Date.now();
|
||||
// 执行下一个 Web 中间件,最后执行到控制器
|
||||
// 执行下一个 Web 中间件,最后执行到控制器
|
||||
// 这里可以拿到下一个中间件或者控制器的返回值
|
||||
const result = await next();
|
||||
// 控制器之后执行的逻辑
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class AreaService {
|
|||
select: 'official',
|
||||
});
|
||||
|
||||
// 如果找不到对应的国家,则抛出错误
|
||||
// 如果找不到对应的国家,则抛出错误
|
||||
if (!name) {
|
||||
throw new Error(`无效的国家代码: ${createAreaDTO.code}`);
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ export class AreaService {
|
|||
return null;
|
||||
}
|
||||
|
||||
// 如果 code 发生变化,则更新 name
|
||||
// 如果 code 发生变化,则更新 name
|
||||
if (updateAreaDTO.code && updateAreaDTO.code !== area.code) {
|
||||
const name = countries.getName(updateAreaDTO.code, 'zh', {
|
||||
select: 'official',
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export class CanadaPostService {
|
|||
return builder.buildObject(xmlObj);
|
||||
}
|
||||
|
||||
// 默认直接构建(用于 createShipment 这类已有完整结构)
|
||||
// 默认直接构建(用于 createShipment 这类已有完整结构)
|
||||
return builder.buildObject(data);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository, Like, In } from 'typeorm';
|
||||
import { Category } from '../entity/category.entity';
|
||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||
import { Dict } from '../entity/dict.entity';
|
||||
|
||||
@Provide()
|
||||
export class CategoryService {
|
||||
@InjectEntityModel(Category)
|
||||
categoryModel: Repository<Category>;
|
||||
|
||||
@InjectEntityModel(CategoryAttribute)
|
||||
categoryAttributeModel: Repository<CategoryAttribute>;
|
||||
|
||||
@InjectEntityModel(Dict)
|
||||
dictModel: Repository<Dict>;
|
||||
|
||||
async getAll() {
|
||||
return await this.categoryModel.find({
|
||||
order: {
|
||||
sort: 'DESC',
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getList(options: { current?: number; pageSize?: number }, name?: string) {
|
||||
const { current = 1, pageSize = 10 } = options;
|
||||
const where = name ? [
|
||||
{ title: Like(`%${name}%`) },
|
||||
{ name: Like(`%${name}%`) }
|
||||
] : [];
|
||||
|
||||
const [list, total] = await this.categoryModel.findAndCount({
|
||||
where: where.length ? where : undefined,
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
order: {
|
||||
sort: 'DESC',
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
async create(data: Partial<Category>) {
|
||||
return await this.categoryModel.save(data);
|
||||
}
|
||||
|
||||
async update(id: number, data: Partial<Category>) {
|
||||
await this.categoryModel.update(id, data);
|
||||
return await this.categoryModel.findOneBy({ id });
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
return await this.categoryModel.delete(id);
|
||||
}
|
||||
|
||||
async findByName(name: string) {
|
||||
return await this.categoryModel.findOneBy({ name });
|
||||
}
|
||||
|
||||
async getCategoryAttributes(categoryId: number) {
|
||||
const categoryAttributes = await this.categoryAttributeModel.find({
|
||||
where: { category: { id: categoryId } },
|
||||
relations: ['attributeDict', 'category'],
|
||||
});
|
||||
|
||||
return categoryAttributes.map(ca => ca.attributeDict);
|
||||
}
|
||||
|
||||
async createCategoryAttribute(categoryId: number, attributeDictIds: number[]) {
|
||||
const category = await this.categoryModel.findOneBy({ id: categoryId });
|
||||
if (!category) throw new Error('分类不存在');
|
||||
|
||||
const dicts = await this.dictModel.findBy({ id: In(attributeDictIds) });
|
||||
if (dicts.length !== attributeDictIds.length) throw new Error('部分属性字典不存在');
|
||||
|
||||
// 检查是否已存在
|
||||
const exist = await this.categoryAttributeModel.find({
|
||||
where: {
|
||||
category: { id: categoryId },
|
||||
attributeDict: { id: In(attributeDictIds) }
|
||||
},
|
||||
relations: ['attributeDict']
|
||||
});
|
||||
|
||||
const existIds = exist.map(e => e.attributeDict.id);
|
||||
const newIds = attributeDictIds.filter(id => !existIds.includes(id));
|
||||
|
||||
const newRecords = newIds.map(id => {
|
||||
const record = new CategoryAttribute();
|
||||
record.category = category;
|
||||
record.attributeDict = dicts.find(d => d.id === id);
|
||||
return record;
|
||||
});
|
||||
|
||||
return await this.categoryAttributeModel.save(newRecords);
|
||||
}
|
||||
|
||||
async deleteCategoryAttribute(id: number) {
|
||||
return await this.categoryAttributeModel.delete(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +100,7 @@ export class DictService {
|
|||
}
|
||||
return this.dictModel.findOne({ where, relations });
|
||||
}
|
||||
// 获取字典列表,支持按标题搜索
|
||||
// 获取字典列表,支持按标题搜索
|
||||
async getDicts(options: { title?: string; name?: string; }) {
|
||||
const where = {
|
||||
title: options.title ? Like(`%${options.title}%`) : undefined,
|
||||
|
|
@ -135,7 +135,7 @@ export class DictService {
|
|||
return result.affected > 0;
|
||||
}
|
||||
|
||||
// 获取字典项列表,支持按 dictId 过滤
|
||||
// 获取字典项列表,支持按 dictId 过滤
|
||||
async getDictItems(params: { dictId?: number; name?: string; title?: string; }) {
|
||||
const { dictId, name, title } = params;
|
||||
const where: any = {};
|
||||
|
|
@ -150,11 +150,11 @@ export class DictService {
|
|||
where.title = Like(`%${title}%`);
|
||||
}
|
||||
|
||||
// 如果提供了 dictId,则只返回该字典下的项
|
||||
// 如果提供了 dictId,则只返回该字典下的项
|
||||
if (params.dictId) {
|
||||
return this.dictItemModel.find({ where });
|
||||
}
|
||||
// 否则,返回所有字典项
|
||||
// 否则,返回所有字典项
|
||||
return this.dictItemModel.find();
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ export class DictService {
|
|||
async getDictItemsByDictName(dictName: string) {
|
||||
// 查找字典
|
||||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||
// 如果字典不存在,则返回空数组
|
||||
// 如果字典不存在,则返回空数组
|
||||
if (!dict) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export class LogisticsService {
|
|||
async removeShipment(shipmentId: number) {
|
||||
try {
|
||||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
|
||||
if (shipment.state !== '190') { // todo,写常数
|
||||
if (shipment.state !== '190') { // todo,写常数
|
||||
throw new Error('订单当前状态无法删除');
|
||||
}
|
||||
const order: Order = await this.orderModel.findOneBy({ id: shipment.order_id });
|
||||
|
|
@ -347,7 +347,7 @@ export class LogisticsService {
|
|||
// 添加运单
|
||||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||
|
||||
// 记录物流信息,并将订单状态转到完成
|
||||
// 记录物流信息,并将订单状态转到完成
|
||||
if (resShipmentOrder.status === 'SUCCESS') {
|
||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||
} else {
|
||||
|
|
@ -359,7 +359,7 @@ export class LogisticsService {
|
|||
await dataSource.transaction(async manager => {
|
||||
const orderRepo = manager.getRepository(Order);
|
||||
const shipmentRepo = manager.getRepository(Shipment);
|
||||
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
||||
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
||||
|
||||
// 同步物流信息到woocommerce
|
||||
const site = await this.siteService.get(Number(order.siteId), true);
|
||||
|
|
@ -414,7 +414,7 @@ export class LogisticsService {
|
|||
if (resShipmentOrder.status === 'SUCCESS') {
|
||||
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 { 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]));
|
||||
|
|
@ -602,7 +602,7 @@ export class LogisticsService {
|
|||
values.push(stockPointId);
|
||||
}
|
||||
|
||||
// todo,增加订单号搜索
|
||||
// todo,增加订单号搜索
|
||||
if (externalOrderId) {
|
||||
whereClause += ' AND o.externalOrderId = ?';
|
||||
values.push(externalOrderId);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export class OrderService {
|
|||
[OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold
|
||||
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
|
||||
}
|
||||
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
|
||||
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
|
||||
async autoUpdateOrderStatus(siteId: number, order: any) {
|
||||
console.log('更新订单状态', order)
|
||||
// 其他状态保持不变
|
||||
|
|
@ -130,14 +130,14 @@ export class OrderService {
|
|||
}
|
||||
try {
|
||||
const site = await this.siteService.get(siteId);
|
||||
// 将订单状态同步到 WooCommerce,然后切换至下一状态
|
||||
// 将订单状态同步到 WooCommerce,然后切换至下一状态
|
||||
await this.wpService.updateOrder(site, String(order.id), { status: order.status });
|
||||
order.status = this.orderAutoNextStatusMap[originStatus];
|
||||
} catch (error) {
|
||||
console.error('更新订单状态失败,原因为:', error)
|
||||
console.error('更新订单状态失败,原因为:', error)
|
||||
}
|
||||
}
|
||||
// wordpress 发来,
|
||||
// wordpress 发来,
|
||||
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
|
||||
let {
|
||||
line_items,
|
||||
|
|
@ -275,7 +275,7 @@ export class OrderService {
|
|||
'PENDING_RESHIPMENT',
|
||||
'PENDING_REFUND',
|
||||
];
|
||||
// 如果当前 ERP 状态不可覆盖,则禁止更新
|
||||
// 如果当前 ERP 状态不可覆盖,则禁止更新
|
||||
return !nonOverridableStatuses.includes(currentErpStatus);
|
||||
}
|
||||
|
||||
|
|
@ -732,7 +732,7 @@ export class OrderService {
|
|||
totalQuery += ` AND o.date_created <= ?`;
|
||||
parameters.push(endDate);
|
||||
}
|
||||
// 支付方式筛选(使用参数化,避免SQL注入)
|
||||
// 支付方式筛选(使用参数化,避免SQL注入)
|
||||
if (payment_method) {
|
||||
sqlQuery += ` AND o.payment_method LIKE ?`;
|
||||
totalQuery += ` AND o.payment_method LIKE ?`;
|
||||
|
|
@ -764,7 +764,7 @@ export class OrderService {
|
|||
}
|
||||
}
|
||||
|
||||
// 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储)
|
||||
// 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储)
|
||||
if (isSubscriptionOnly) {
|
||||
const subCond = `
|
||||
AND (
|
||||
|
|
@ -862,7 +862,7 @@ export class OrderService {
|
|||
customer_email: `%${customer_email}%`,
|
||||
});
|
||||
|
||||
// 🔥 关键字搜索:检查 order_item.name 是否包含 keyword
|
||||
// 🔥 关键字搜索:检查 order_item.name 是否包含 keyword
|
||||
if (keyword) {
|
||||
query.andWhere(
|
||||
`EXISTS (
|
||||
|
|
@ -1025,7 +1025,7 @@ export class OrderService {
|
|||
}
|
||||
|
||||
// -------------------------
|
||||
// 4. 总量统计(时间段 + siteId)
|
||||
// 4. 总量统计(时间段 + siteId)
|
||||
// -------------------------
|
||||
const totalParams: any[] = [startDate, endDate];
|
||||
const yooneParams: any[] = [startDate, endDate];
|
||||
|
|
@ -1304,7 +1304,7 @@ export class OrderService {
|
|||
|
||||
|
||||
|
||||
// 关联数据:订阅与相关订单(用于前端关联展示)
|
||||
// 关联数据:订阅与相关订单(用于前端关联展示)
|
||||
let relatedList: any[] = [];
|
||||
try {
|
||||
const related = await this.getRelatedByOrder(id);
|
||||
|
|
@ -1330,7 +1330,7 @@ export class OrderService {
|
|||
return {
|
||||
...order,
|
||||
name: site?.name,
|
||||
// Site 实体无邮箱字段,这里返回空字符串保持兼容
|
||||
// Site 实体无邮箱字段,这里返回空字符串保持兼容
|
||||
email: '',
|
||||
items,
|
||||
sales,
|
||||
|
|
@ -1394,7 +1394,7 @@ export class OrderService {
|
|||
]),
|
||||
},
|
||||
});
|
||||
// 批量获取订单涉及的站点名称,避免使用配置文件
|
||||
// 批量获取订单涉及的站点名称,避免使用配置文件
|
||||
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 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>) {
|
||||
// 从数据中解构出需要用的属性
|
||||
const { siteId, sales, total, billing, customer_email, billing_phone } = data;
|
||||
// 如果没有 siteId,则抛出错误
|
||||
// 如果没有 siteId,则抛出错误
|
||||
if (!siteId) {
|
||||
throw new Error('siteId is required');
|
||||
}
|
||||
|
|
@ -1573,11 +1573,11 @@ export class OrderService {
|
|||
});
|
||||
|
||||
if (transactionError !== undefined) {
|
||||
throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`更新发货产品失败:${error.message}`);
|
||||
throw new Error(`更新发货产品失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1658,11 +1658,11 @@ export class OrderService {
|
|||
});
|
||||
|
||||
if (transactionError !== undefined) {
|
||||
throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`更新发货产品失败:${error.message}`);
|
||||
throw new Error(`更新发货产品失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,8 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
|
|||
import { Repository, Like, In } from 'typeorm';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { WpSite } from '../interface';
|
||||
import { UpdateSiteDTO } from '../dto/site.dto';
|
||||
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||
import { Area } from '../entity/area.entity';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
|
|
@ -11,11 +12,16 @@ export class SiteService {
|
|||
@InjectEntityModel(Site)
|
||||
siteModel: Repository<Site>;
|
||||
|
||||
@InjectEntityModel(Area)
|
||||
areaModel: Repository<Area>;
|
||||
|
||||
async syncFromConfig(sites: WpSite[] = []) {
|
||||
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
||||
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
||||
for (const siteConfig of sites) {
|
||||
// 按站点名称查询是否已存在记录
|
||||
const exist = await this.siteModel.findOne({ where: { name: siteConfig.name } });
|
||||
const exist = await this.siteModel.findOne({
|
||||
where: { name: siteConfig.name },
|
||||
});
|
||||
// 将 WpSite 字段映射为 Site 实体字段
|
||||
const payload: Partial<Site> = {
|
||||
name: siteConfig.name,
|
||||
|
|
@ -24,64 +30,143 @@ export class SiteService {
|
|||
consumerSecret: (siteConfig as any).consumerSecret,
|
||||
type: 'woocommerce',
|
||||
};
|
||||
// 存在则更新,不存在则插入新记录
|
||||
if (exist) await this.siteModel.update({ id: exist.id }, payload);
|
||||
else await this.siteModel.insert(payload as Site);
|
||||
// 存在则更新,不存在则插入新记录
|
||||
if (exist) {
|
||||
await this.siteModel.update({ id: exist.id }, payload);
|
||||
} else {
|
||||
await this.siteModel.insert(payload as Site);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create(data: Partial<Site>) {
|
||||
// 创建新的站点记录
|
||||
await this.siteModel.insert(data as Site);
|
||||
async create(data: CreateSiteDTO) {
|
||||
// 从 DTO 中分离出区域代码和其他站点数据
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
const newSite = new Site();
|
||||
Object.assign(newSite, restData);
|
||||
|
||||
// 如果传入了区域代码,则查询并关联 Area 实体
|
||||
if (areaCodes && areaCodes.length > 0) {
|
||||
const areas = await this.areaModel.findBy({
|
||||
code: In(areaCodes),
|
||||
});
|
||||
newSite.areas = areas;
|
||||
} else {
|
||||
// 如果没有传入区域,则关联一个空数组,代表"全局"
|
||||
newSite.areas = [];
|
||||
}
|
||||
|
||||
// 使用 save 方法保存实体及其关联关系
|
||||
await this.siteModel.save(newSite);
|
||||
return true;
|
||||
}
|
||||
|
||||
async update(id: string | number, data: UpdateSiteDTO) {
|
||||
// 更新指定站点记录,将布尔 isDisabled 转换为数值 0/1
|
||||
// 从 DTO 中分离出区域代码和其他站点数据
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
|
||||
// 首先,根据 ID 查找要更新的站点实体
|
||||
const siteToUpdate = await this.siteModel.findOne({
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
if (!siteToUpdate) {
|
||||
// 如果找不到站点,则操作失败
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新站点的基本字段
|
||||
const payload: Partial<Site> = {
|
||||
...data,
|
||||
...restData,
|
||||
isDisabled:
|
||||
data.isDisabled === undefined // 未传入则不更新该字段
|
||||
data.isDisabled === undefined
|
||||
? undefined
|
||||
: data.isDisabled // true -> 1, false -> 0
|
||||
: data.isDisabled
|
||||
? 1
|
||||
: 0,
|
||||
} as any;
|
||||
await this.siteModel.update({ id: Number(id) }, payload);
|
||||
Object.assign(siteToUpdate, payload);
|
||||
|
||||
// 如果 DTO 中传入了 areas 字段(即使是空数组),也要更新关联关系
|
||||
if (areaCodes !== undefined) {
|
||||
if (areaCodes.length > 0) {
|
||||
// 如果区域代码数组不为空,则查找并更新关联
|
||||
const areas = await this.areaModel.findBy({
|
||||
code: In(areaCodes),
|
||||
});
|
||||
siteToUpdate.areas = areas;
|
||||
} else {
|
||||
// 如果传入空数组,则清空所有关联,代表"全局"
|
||||
siteToUpdate.areas = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 save 方法保存实体及其更新后的关联关系
|
||||
await this.siteModel.save(siteToUpdate);
|
||||
return true;
|
||||
}
|
||||
|
||||
async get(id: string | number, includeSecret = false) {
|
||||
// 根据主键获取站点;includeSecret 为 true 时返回密钥字段
|
||||
const site = await this.siteModel.findOne({ where: { id: Number(id) } });
|
||||
if (!site) return null;
|
||||
if (includeSecret) return site;
|
||||
// 默认不返回密钥,进行字段脱敏
|
||||
// 根据主键获取站点,并使用 relations 加载关联的 areas
|
||||
const site = await this.siteModel.findOne({
|
||||
where: { id: Number(id) },
|
||||
relations: ['areas'],
|
||||
});
|
||||
if (!site) {
|
||||
return null;
|
||||
}
|
||||
// 如果需要包含密钥,则直接返回
|
||||
if (includeSecret) {
|
||||
return site;
|
||||
}
|
||||
// 默认不返回密钥,进行字段脱敏
|
||||
const { consumerKey, consumerSecret, ...rest } = site;
|
||||
return rest;
|
||||
}
|
||||
|
||||
async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) {
|
||||
// 分页查询站点列表,支持关键字、禁用状态与 ID 列表过滤
|
||||
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any;
|
||||
async list(
|
||||
param: {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
isDisabled?: boolean;
|
||||
ids?: string;
|
||||
},
|
||||
includeSecret = false
|
||||
) {
|
||||
// 分页查询站点列表,支持关键字,禁用状态与 ID 列表过滤
|
||||
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param ||
|
||||
{}) as any;
|
||||
const where: any = {};
|
||||
// 按名称模糊查询
|
||||
if (keyword) where.name = Like(`%${keyword}%`);
|
||||
// 按禁用状态过滤(布尔转数值)
|
||||
if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0;
|
||||
if (keyword) {
|
||||
where.name = Like(`%${keyword}%`);
|
||||
}
|
||||
// 按禁用状态过滤(布尔转数值)
|
||||
if (typeof isDisabled === 'boolean') {
|
||||
where.isDisabled = isDisabled ? 1 : 0;
|
||||
}
|
||||
if (ids) {
|
||||
// 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
|
||||
// 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
|
||||
const numIds = String(ids)
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.map((i) => Number(i))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
if (numIds.length > 0) where.id = In(numIds);
|
||||
.map(i => Number(i))
|
||||
.filter(v => !Number.isNaN(v));
|
||||
if (numIds.length > 0) {
|
||||
where.id = In(numIds);
|
||||
}
|
||||
// 进行分页查询(skip/take)并返回总条数
|
||||
const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize });
|
||||
}
|
||||
// 进行分页查询,并使用 relations 加载关联的 areas
|
||||
const [items, total] = await this.siteModel.findAndCount({
|
||||
where,
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
relations: ['areas'],
|
||||
});
|
||||
// 根据 includeSecret 决定是否脱敏返回密钥字段
|
||||
const data = includeSecret ? items : items.map((item: any) => {
|
||||
const data = includeSecret
|
||||
? items
|
||||
: items.map((item: any) => {
|
||||
const { consumerKey, consumerSecret, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
|
|
@ -89,7 +174,7 @@ export class SiteService {
|
|||
}
|
||||
|
||||
async disable(id: string | number, disabled: boolean) {
|
||||
// 设置站点禁用状态(true -> 1, false -> 0)
|
||||
// 设置站点禁用状态(true -> 1, false -> 0)
|
||||
await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled });
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1011,7 +1011,7 @@ export class StatisticsService {
|
|||
GROUP BY customer_email
|
||||
),
|
||||
|
||||
-- 标注每个用户每月是“新客户”还是“老客户”
|
||||
-- 标注每个用户每月是"新客户"还是"老客户"
|
||||
labeled_users AS (
|
||||
SELECT
|
||||
m.customer_email,
|
||||
|
|
@ -1056,7 +1056,7 @@ export class StatisticsService {
|
|||
GROUP BY current_month
|
||||
)
|
||||
|
||||
-- 最终结果:每月新客户、老客户、未来未复购客户
|
||||
-- 最终结果:每月新客户,老客户,未来未复购客户
|
||||
SELECT
|
||||
m.order_month,
|
||||
m.new_user_count,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { Between, Like, Repository, LessThan, MoreThan } from 'typeorm';
|
||||
import { Between, Like, Repository, LessThan, MoreThan, In } from 'typeorm';
|
||||
import { Stock } from '../entity/stock.entity';
|
||||
import { StockRecord } from '../entity/stock_record.entity';
|
||||
import { paginate } from '../utils/paginate.util';
|
||||
|
|
@ -27,6 +27,7 @@ import { User } from '../entity/user.entity';
|
|||
import dayjs = require('dayjs');
|
||||
import { Transfer } from '../entity/transfer.entity';
|
||||
import { TransferItem } from '../entity/transfer_item.entity';
|
||||
import { Area } from '../entity/area.entity';
|
||||
|
||||
@Provide()
|
||||
export class StockService {
|
||||
|
|
@ -51,35 +52,55 @@ export class StockService {
|
|||
@InjectEntityModel(TransferItem)
|
||||
transferItemModel: Repository<TransferItem>;
|
||||
|
||||
@InjectEntityModel(Area)
|
||||
areaModel: Repository<Area>;
|
||||
|
||||
async createStockPoint(data: CreateStockPointDTO) {
|
||||
const { name, location, contactPerson, contactPhone } = data;
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
const stockPoint = new StockPoint();
|
||||
stockPoint.name = name;
|
||||
stockPoint.location = location;
|
||||
stockPoint.contactPerson = contactPerson;
|
||||
stockPoint.contactPhone = contactPhone;
|
||||
Object.assign(stockPoint, restData);
|
||||
|
||||
if (areaCodes && areaCodes.length > 0) {
|
||||
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
|
||||
stockPoint.areas = areas;
|
||||
} else {
|
||||
stockPoint.areas = [];
|
||||
}
|
||||
|
||||
await this.stockPointModel.save(stockPoint);
|
||||
}
|
||||
|
||||
async updateStockPoint(id: number, data: UpdateStockPointDTO) {
|
||||
// 确认产品是否存在
|
||||
const point = await this.stockPointModel.findOneBy({ id });
|
||||
if (!point) {
|
||||
throw new Error(`产品 ID ${id} 不存在`);
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
const pointToUpdate = await this.stockPointModel.findOneBy({ id });
|
||||
if (!pointToUpdate) {
|
||||
throw new Error(`仓库点 ID ${id} 不存在`);
|
||||
}
|
||||
// 更新产品
|
||||
await this.stockPointModel.update(id, data);
|
||||
|
||||
Object.assign(pointToUpdate, restData);
|
||||
|
||||
if (areaCodes !== undefined) {
|
||||
if (areaCodes.length > 0) {
|
||||
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
|
||||
pointToUpdate.areas = areas;
|
||||
} else {
|
||||
pointToUpdate.areas = [];
|
||||
}
|
||||
}
|
||||
|
||||
await this.stockPointModel.save(pointToUpdate);
|
||||
}
|
||||
|
||||
async getStockPoints(query: QueryPointDTO) {
|
||||
const { current = 1, pageSize = 10 } = query;
|
||||
return await paginate(this.stockPointModel, {
|
||||
pagination: { current, pageSize },
|
||||
relations: ['areas'],
|
||||
});
|
||||
}
|
||||
|
||||
async getAllStockPoints(): Promise<StockPoint[]> {
|
||||
return await this.stockPointModel.find();
|
||||
return await this.stockPointModel.find({ relations: ['areas'] });
|
||||
}
|
||||
|
||||
async delStockPoints(id: number) {
|
||||
|
|
@ -118,7 +139,7 @@ export class StockService {
|
|||
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
|
||||
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
|
||||
if (purchaseOrder.status === 'received')
|
||||
throw new Error(`采购订单 ID ${id} 已到达,无法修改`);
|
||||
throw new Error(`采购订单 ID ${id} 已到达,无法修改`);
|
||||
const { stockPointId, expectedArrivalTime, status, items, note } = data;
|
||||
purchaseOrder.stockPointId = stockPointId;
|
||||
purchaseOrder.expectedArrivalTime = expectedArrivalTime;
|
||||
|
|
@ -187,7 +208,7 @@ export class StockService {
|
|||
);
|
||||
}
|
||||
|
||||
// 中文注释:检查指定 SKU 是否在任一仓库有库存(数量大于 0)
|
||||
// 检查指定 SKU 是否在任一仓库有库存(数量大于 0)
|
||||
async hasStockBySku(sku: string): Promise<boolean> {
|
||||
const count = await this.stockModel
|
||||
.createQueryBuilder('stock')
|
||||
|
|
@ -201,7 +222,7 @@ export class StockService {
|
|||
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
|
||||
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
|
||||
if (purchaseOrder.status === 'received')
|
||||
throw new Error(`采购订单 ID ${id} 已到达,无法删除`);
|
||||
throw new Error(`采购订单 ID ${id} 已到达,无法删除`);
|
||||
await this.purchaseOrderItemModel.delete({ purchaseOrderId: id });
|
||||
await this.purchaseOrderModel.delete({ id });
|
||||
}
|
||||
|
|
@ -210,7 +231,7 @@ export class StockService {
|
|||
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
|
||||
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
|
||||
if (purchaseOrder.status === 'received')
|
||||
throw new Error(`采购订单 ID ${id} 已到达,不要重复操作`);
|
||||
throw new Error(`采购订单 ID ${id} 已到达,不要重复操作`);
|
||||
const items = await this.purchaseOrderItemModel.find({
|
||||
where: { purchaseOrderId: id },
|
||||
});
|
||||
|
|
@ -382,7 +403,7 @@ export class StockService {
|
|||
sku,
|
||||
});
|
||||
if (!stock) {
|
||||
// 如果库存不存在,则直接新增
|
||||
// 如果库存不存在,则直接新增
|
||||
const newStock = this.stockModel.create({
|
||||
stockPointId,
|
||||
sku,
|
||||
|
|
@ -394,7 +415,7 @@ export class StockService {
|
|||
stock.quantity +=
|
||||
operationType === 'in' ? quantityChange : -quantityChange;
|
||||
// if (stock.quantity < 0) {
|
||||
// throw new Error('库存不足,无法完成操作');
|
||||
// throw new Error('库存不足,无法完成操作');
|
||||
// }
|
||||
await this.stockModel.save(stock);
|
||||
}
|
||||
|
|
@ -550,7 +571,7 @@ export class StockService {
|
|||
async cancelTransfer(id: number, userId: number) {
|
||||
const transfer = await this.transferModel.findOneBy({ 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({
|
||||
where: { transferId: id },
|
||||
});
|
||||
|
|
@ -573,7 +594,7 @@ export class StockService {
|
|||
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
|
||||
if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`);
|
||||
if (transfer.isArrived)
|
||||
throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
|
||||
throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
|
||||
const items = await this.transferItemModel.find({
|
||||
where: { transferId: id },
|
||||
});
|
||||
|
|
@ -596,7 +617,7 @@ export class StockService {
|
|||
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
|
||||
if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`);
|
||||
if (transfer.isArrived)
|
||||
throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
|
||||
throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
|
||||
transfer.isLost = true;
|
||||
await this.transferModel.save(transfer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export class SubscriptionService {
|
|||
|
||||
/**
|
||||
* 同步单条订阅
|
||||
* - 规范化字段、设置幂等键 externalSubscriptionId
|
||||
* - 已存在则更新,不存在则新增
|
||||
* - 规范化字段,设置幂等键 externalSubscriptionId
|
||||
* - 已存在则更新,不存在则新增
|
||||
*/
|
||||
async syncSingleSubscription(siteId: number, sub: any) {
|
||||
const { line_items, ...raw } = sub;
|
||||
|
|
@ -54,7 +54,7 @@ export class SubscriptionService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取订阅分页列表(支持站点、状态、邮箱与关键字筛选)
|
||||
* 获取订阅分页列表(支持站点,状态,邮箱与关键字筛选)
|
||||
*/
|
||||
async getSubscriptionList({
|
||||
current = 1,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
|
|||
import { Repository } from 'typeorm';
|
||||
import { Template } from '../entity/template.entity';
|
||||
import { CreateTemplateDTO, UpdateTemplateDTO } from '../dto/template.dto';
|
||||
import { Eta } from 'eta';
|
||||
|
||||
/**
|
||||
* @service TemplateService 模板服务
|
||||
*/
|
||||
@Provide()
|
||||
export class TemplateService {
|
||||
private eta = new Eta();
|
||||
|
||||
// 注入 Template 实体模型
|
||||
@InjectEntityModel(Template)
|
||||
templateModel: Repository<Template>;
|
||||
|
|
@ -64,7 +67,7 @@ export class TemplateService {
|
|||
): Promise<Template> {
|
||||
// 首先根据 ID 查找模板
|
||||
const template = await this.templateModel.findOneBy({ id });
|
||||
// 如果模板不存在,则抛出错误
|
||||
// 如果模板不存在,则抛出错误
|
||||
if (!template) {
|
||||
throw new Error(`模板 ID ${id} 不存在`);
|
||||
}
|
||||
|
|
@ -78,22 +81,22 @@ export class TemplateService {
|
|||
/**
|
||||
* 根据 ID 删除一个模板
|
||||
* @param {number} id - 模板 ID
|
||||
* @returns {Promise<boolean>} 如果删除成功则返回 true,否则返回 false
|
||||
* @returns {Promise<boolean>} 如果删除成功则返回 true,否则返回 false
|
||||
*/
|
||||
async deleteTemplate(id: number): Promise<boolean> {
|
||||
// 首先根据 ID 查找模板
|
||||
const template = await this.templateModel.findOneBy({ id });
|
||||
// 如果模板不存在,则抛出错误
|
||||
// 如果模板不存在,则抛出错误
|
||||
if (!template) {
|
||||
throw new Error(`模板 ID ${id} 不存在`);
|
||||
}
|
||||
// 如果模板不可删除,则抛出错误
|
||||
// 如果模板不可删除,则抛出错误
|
||||
if (!template.deletable) {
|
||||
throw new Error(`模板 ${template.name} 不可删除`);
|
||||
}
|
||||
// 执行删除操作
|
||||
const result = await this.templateModel.delete(id);
|
||||
// 如果影响的行数大于 0,则表示删除成功
|
||||
// 如果影响的行数大于 0,则表示删除成功
|
||||
return result.affected > 0;
|
||||
}
|
||||
|
||||
|
|
@ -106,22 +109,12 @@ export class TemplateService {
|
|||
async render(name: string, data: Record<string, any>): Promise<string> {
|
||||
// 根据名称获取模板
|
||||
const template = await this.getTemplateByName(name);
|
||||
// 如果模板不存在,则抛出错误
|
||||
// 如果模板不存在,则抛出错误
|
||||
if (!template) {
|
||||
throw new Error(`模板 '${name}' 不存在`);
|
||||
}
|
||||
|
||||
// 获取模板的原始内容
|
||||
let rendered = template.value;
|
||||
// 遍历数据对象,替换模板中的占位符
|
||||
for (const key in data) {
|
||||
// 创建一个正则表达式来匹配 {{key}}
|
||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
||||
// 执行替换操作
|
||||
rendered = rendered.replace(regex, data[key]);
|
||||
}
|
||||
|
||||
// 返回渲染后的字符串
|
||||
return rendered;
|
||||
// 使用 Eta 渲染
|
||||
return this.eta.renderString(template.value, data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export class UniExpressService {
|
|||
|
||||
async getOrderStatus(tracking_number: string) {
|
||||
try {
|
||||
const key = 'SMq45nJhQuNR3WHsJA6N'; // todo,写进常数
|
||||
const key = 'SMq45nJhQuNR3WHsJA6N'; // todo,写进常数
|
||||
const config: AxiosRequestConfig= {
|
||||
method: 'GET',
|
||||
url: `${this.url}/orders/trackinguniuni`,
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ export class UserService {
|
|||
throw new Error('验证码错误');
|
||||
}
|
||||
|
||||
// 校验通过后,将设备加入白名单
|
||||
// 校验通过后,将设备加入白名单
|
||||
await this.deviceWhitelistService.addToWhitelist(deviceId);
|
||||
}
|
||||
|
||||
// 生成 JWT,包含角色和权限信息
|
||||
// 生成 JWT,包含角色和权限信息
|
||||
const token = await this.jwtService.sign({
|
||||
id: user.id,
|
||||
deviceId,
|
||||
|
|
@ -81,7 +81,7 @@ export class UserService {
|
|||
};
|
||||
}
|
||||
|
||||
// 中文注释:新增用户(支持可选备注)
|
||||
// 新增用户(支持可选备注)
|
||||
async addUser(username: string, password: string, remark?: string) {
|
||||
const existingUser = await this.userModel.findOne({
|
||||
where: { username },
|
||||
|
|
@ -93,13 +93,13 @@ export class UserService {
|
|||
const user = this.userModel.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
// 中文注释:备注字段赋值(若提供)
|
||||
// 备注字段赋值(若提供)
|
||||
...(remark ? { remark } : {}),
|
||||
});
|
||||
return this.userModel.save(user);
|
||||
}
|
||||
|
||||
// 中文注释:用户列表支持分页与备注模糊查询(以及可选的布尔过滤)
|
||||
// 用户列表支持分页与备注模糊查询(以及可选的布尔过滤)
|
||||
async listUsers(
|
||||
current: number,
|
||||
pageSize: number,
|
||||
|
|
@ -111,13 +111,13 @@ export class UserService {
|
|||
isAdmin?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
// 条件判断:构造 where 条件
|
||||
// 条件判断:构造 where 条件
|
||||
const where: Record<string, any> = {};
|
||||
if (filters.username) where.username = Like(`%${filters.username}%`); // 中文注释:用户名精确匹配(如需模糊可改为 Like)
|
||||
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 中文注释:按启用状态过滤
|
||||
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 中文注释:按超管过滤
|
||||
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 中文注释:按管理员过滤
|
||||
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 中文注释:备注模糊搜索
|
||||
if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
|
||||
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
|
||||
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
|
||||
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
|
||||
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
|
||||
|
||||
const [items, total] = await this.userModel.findAndCount({
|
||||
where,
|
||||
|
|
@ -137,7 +137,7 @@ export class UserService {
|
|||
return this.userModel.save(user);
|
||||
}
|
||||
|
||||
// 中文注释:更新用户信息(支持用户名唯一校验与可选密码修改)
|
||||
// 更新用户信息(支持用户名唯一校验与可选密码修改)
|
||||
async updateUser(
|
||||
userId: number,
|
||||
payload: {
|
||||
|
|
@ -149,30 +149,30 @@ export class UserService {
|
|||
remark?: string;
|
||||
}
|
||||
) {
|
||||
// 条件判断:查询用户是否存在
|
||||
// 条件判断:查询用户是否存在
|
||||
const user = await this.userModel.findOne({ where: { id: userId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
// 条件判断:若提供了新用户名且与原用户名不同,校验唯一性
|
||||
// 条件判断:若提供了新用户名且与原用户名不同,校验唯一性
|
||||
if (payload.username && payload.username !== user.username) {
|
||||
const exist = await this.userModel.findOne({ where: { username: payload.username } });
|
||||
if (exist) throw new Error('用户名已存在');
|
||||
user.username = payload.username;
|
||||
}
|
||||
|
||||
// 条件判断:若提供密码则进行加密存储
|
||||
// 条件判断:若提供密码则进行加密存储
|
||||
if (payload.password) {
|
||||
user.password = await bcrypt.hash(payload.password, 10);
|
||||
}
|
||||
|
||||
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
||||
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
||||
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
||||
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
||||
if (Array.isArray(payload.permissions)) user.permissions = payload.permissions;
|
||||
|
||||
// 条件判断:更新备注(若提供则覆盖)
|
||||
// 条件判断:更新备注(若提供则覆盖)
|
||||
if (typeof payload.remark === 'string') user.remark = payload.remark;
|
||||
|
||||
// 保存更新
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ export class WPService {
|
|||
private readonly siteService: SiteService;
|
||||
|
||||
/**
|
||||
* 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败
|
||||
* 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId)
|
||||
* 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败
|
||||
* 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId)
|
||||
*/
|
||||
private buildURL(base: string, ...parts: Array<string | number>): string {
|
||||
// 去掉 base 末尾多余斜杠,但不影响协议中的 //
|
||||
// 去掉 base 末尾多余斜杠,但不影响协议中的 //
|
||||
const baseSanitized = String(base).replace(/\/+$/g, '');
|
||||
// 规范各段前后斜杠
|
||||
const segments = parts
|
||||
|
|
@ -26,14 +26,14 @@ export class WPService {
|
|||
.map((s) => s.replace(/^\/+|\/+$/g, ''))
|
||||
.filter(Boolean);
|
||||
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/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WooCommerce SDK 实例
|
||||
* @param site 站点配置
|
||||
* @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1
|
||||
* @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1
|
||||
*/
|
||||
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
|
||||
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> = {}) {
|
||||
const page = params.page ?? 1;
|
||||
const per_page = params.per_page ?? 100;
|
||||
const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page });
|
||||
if (res?.headers?.['content-type']?.includes('text/html')) {
|
||||
throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限');
|
||||
throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限');
|
||||
}
|
||||
const data = res.data as T[];
|
||||
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[]> {
|
||||
const result: T[] = [];
|
||||
|
|
@ -76,7 +76,7 @@ export class WPService {
|
|||
/**
|
||||
* 获取 WordPress 数据
|
||||
* @param wpApiUrl WordPress REST API 的基础地址
|
||||
* @param endpoint API 端点路径(例如 wc/v3/products)
|
||||
* @param endpoint API 端点路径(例如 wc/v3/products)
|
||||
* @param consumerKey WooCommerce 的消费者密钥
|
||||
* @param consumerSecret WooCommerce 的消费者密钥
|
||||
*/
|
||||
|
|
@ -91,7 +91,7 @@ export class WPService {
|
|||
try {
|
||||
const apiUrl = site.apiUrl;
|
||||
const { consumerKey, consumerSecret } = site;
|
||||
// 构建 URL,规避多/或少/问题
|
||||
// 构建 URL,规避多/或少/问题
|
||||
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
|
||||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||||
'base64'
|
||||
|
|
@ -126,7 +126,7 @@ export class WPService {
|
|||
while (hasMore) {
|
||||
const config: AxiosRequestConfig = {
|
||||
method: 'GET',
|
||||
// 构建 URL,规避多/或少/问题
|
||||
// 构建 URL,规避多/或少/问题
|
||||
url: this.buildURL(apiUrl, '/wp-json', endpoint),
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
|
|
@ -194,12 +194,12 @@ export class WPService {
|
|||
|
||||
/**
|
||||
* 获取 WooCommerce Subscriptions
|
||||
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions。
|
||||
* 返回所有分页合并后的订阅数组。
|
||||
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions.
|
||||
* 返回所有分页合并后的订阅数组.
|
||||
*/
|
||||
async getSubscriptions(siteId: number): Promise<Record<string, any>[]> {
|
||||
const site = await this.siteService.get(siteId);
|
||||
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
|
||||
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions');
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ export class WPService {
|
|||
);
|
||||
const config: AxiosRequestConfig = {
|
||||
method: 'PUT',
|
||||
// 构建 URL,规避多/或少/问题
|
||||
// 构建 URL,规避多/或少/问题
|
||||
url: this.buildURL(apiUrl, '/wp-json', endpoint),
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
|
|
@ -304,7 +304,7 @@ export class WPService {
|
|||
): Promise<Boolean> {
|
||||
const res = await this.updateData(`/wc/v3/products/${productId}`, site, {
|
||||
status,
|
||||
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
|
||||
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
|
||||
stock_status,
|
||||
});
|
||||
return res;
|
||||
|
|
@ -357,7 +357,7 @@ export class WPService {
|
|||
);
|
||||
const config: AxiosRequestConfig = {
|
||||
method: 'POST',
|
||||
// 构建 URL,规避多/或少/问题
|
||||
// 构建 URL,规避多/或少/问题
|
||||
url: this.buildURL(
|
||||
apiUrl,
|
||||
'/wp-json',
|
||||
|
|
@ -385,10 +385,10 @@ export class WPService {
|
|||
);
|
||||
|
||||
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 = {
|
||||
method: 'DELETE',
|
||||
// 构建 URL,规避多/或少/问题
|
||||
// 构建 URL,规避多/或少/问题
|
||||
url: this.buildURL(
|
||||
apiUrl,
|
||||
'/wp-json',
|
||||
|
|
@ -403,4 +403,37 @@ export class WPService {
|
|||
};
|
||||
return await axios.request(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WordPress 媒体库数据
|
||||
* @param siteId 站点 ID
|
||||
* @param page 页码
|
||||
* @param perPage 每页数量
|
||||
*/
|
||||
async getMedia(siteId: number, page: number = 1, perPage: number = 20): Promise<{ items: any[], total: number, totalPages: number }> {
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
if (!site) {
|
||||
throw new Error('站点不存在');
|
||||
}
|
||||
const endpoint = 'wp/v2/media';
|
||||
const apiUrl = site.apiUrl;
|
||||
const { consumerKey, consumerSecret } = site as any;
|
||||
// 构建 URL,规避多/或少/问题
|
||||
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
|
||||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
|
||||
|
||||
const response = await axios.get(url, {
|
||||
headers: { Authorization: `Basic ${auth}` },
|
||||
params: { page, per_page: perPage }
|
||||
});
|
||||
|
||||
const total = Number(response.headers['x-wp-total'] || 0);
|
||||
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
||||
|
||||
return {
|
||||
items: response.data,
|
||||
total,
|
||||
totalPages
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import { SiteService } from './site.service';
|
|||
|
||||
@Provide()
|
||||
export class WpProductService {
|
||||
// 移除配置中的站点数组,统一从数据库获取站点信息
|
||||
// 移除配置中的站点数组,统一从数据库获取站点信息
|
||||
|
||||
@Inject()
|
||||
private readonly wpApiService: WPService;
|
||||
|
|
@ -31,7 +31,7 @@ export class WpProductService {
|
|||
|
||||
|
||||
async syncAllSites() {
|
||||
// 从数据库获取所有启用的站点,并逐站点同步产品与变体
|
||||
// 从数据库获取所有启用的站点,并逐站点同步产品与变体
|
||||
const { items: sites } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true);
|
||||
for (const site of sites) {
|
||||
const products = await this.wpApiService.getProducts(site);
|
||||
|
|
@ -46,7 +46,7 @@ export class WpProductService {
|
|||
}
|
||||
// 同步一个网站
|
||||
async syncSite(siteId: number) {
|
||||
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
||||
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
||||
.select([
|
||||
|
|
@ -304,7 +304,7 @@ export class WpProductService {
|
|||
|
||||
async getProductList(param: QueryWpProductDTO) {
|
||||
const { current = 1, pageSize = 10, name, siteId, status } = param;
|
||||
// 第一步:先查询分页的产品
|
||||
// 第一步:先查询分页的产品
|
||||
const where: any = {};
|
||||
if (siteId) {
|
||||
where.siteId = siteId;
|
||||
|
|
@ -376,9 +376,9 @@ export class WpProductService {
|
|||
const items = rawResult.reduce((acc, row) => {
|
||||
// 在累加器中查找当前产品
|
||||
let product = acc.find(p => p.id === row.id);
|
||||
// 如果产品不存在,则创建新产品
|
||||
// 如果产品不存在,则创建新产品
|
||||
if (!product) {
|
||||
// 从原始产品列表中查找,以获取 'site' 关联数据
|
||||
// 从原始产品列表中查找,以获取 'site' 关联数据
|
||||
const originalProduct = products.find(p => p.id === row.id);
|
||||
product = {
|
||||
...Object.keys(row)
|
||||
|
|
@ -535,7 +535,7 @@ export class WpProductService {
|
|||
const params: Record<string, string> = {};
|
||||
const conditions: string[] = [];
|
||||
|
||||
// 英文名关键词全部匹配(AND)
|
||||
// 英文名关键词全部匹配(AND)
|
||||
if (nameFilter.length > 0) {
|
||||
const nameConds = nameFilter.map((word, index) => {
|
||||
const key = `name${index}`;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export async function paginate<T>(
|
|||
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[];
|
||||
relations?: string[];
|
||||
order?: Record<string, 'ASC' | 'DESC'>;
|
||||
transformerClass?: new () => T; // 可选:用于指定需要转换的类
|
||||
transformerClass?: new () => T; // 可选:用于指定需要转换的类
|
||||
}
|
||||
): Promise<PaginationResult<T>> {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"typeRoots": ["./typings", "./node_modules/@types"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"inlineSources": true // ✅ 把源码嵌入 map 文件,方便 VS Code 还原
|
||||
"inlineSources": true // ✅ 把源码嵌入 map 文件,方便 VS Code 还原
|
||||
|
||||
},
|
||||
"exclude": ["*.js", "*.ts", "dist", "node_modules", "test"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue