Compare commits

..

No commits in common. "62f9ca947a3889669c7c3472a3aaa653618cd116" and "f20f4727f6162bf29c7716169f3708597d6d4ca1" have entirely different histories.

63 changed files with 849 additions and 2017 deletions

View File

@ -1,6 +1,6 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings", "scripts"],
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
"env": {
"jest": true
}

2
.gitignore vendored
View File

@ -15,5 +15,3 @@ yarn.lock
**/config.prod.ts
**/config.local.ts
container
scripts
ai

View File

@ -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可能返回的错误信息
- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
- `区域不存在`: 当尝试更新或删除不存在的区域时

View File

@ -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开始为区域添加坐标信息

177
package-lock.json generated
View File

@ -20,7 +20,6 @@
"@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",
@ -28,7 +27,6 @@
"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",
@ -815,19 +813,6 @@
"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",
@ -943,12 +928,6 @@
"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",
@ -1174,18 +1153,6 @@
"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",
@ -2279,36 +2246,6 @@
"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",
@ -2341,23 +2278,6 @@
"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",
@ -3687,19 +3607,6 @@
"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",
@ -3734,15 +3641,6 @@
"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",
@ -3879,47 +3777,6 @@
"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",
@ -4445,23 +4302,6 @@
"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",
@ -4562,23 +4402,6 @@
"node": ">=0.6"
}
},
"node_modules/token-types": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tsc-alias": {
"version": "1.8.16",
"resolved": "https://registry.npmmirror.com/tsc-alias/-/tsc-alias-1.8.16.tgz",

View File

@ -15,7 +15,6 @@
"@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",
@ -23,7 +22,6 @@
"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",

View File

@ -36,11 +36,7 @@ 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
@ -85,18 +81,16 @@ export default {
DictItem,
Template,
Area,
CategoryAttribute,
Category,
],
synchronize: true,
logging: false,
seeders: [DictSeeder, CategorySeeder, CategoryAttributeSeeder],
seeders: [DictSeeder],
},
dataSource: {
default: {
type: 'mysql',
host: 'localhost',
port: 10014,
port: 3306,
username: 'root',
password: 'root',
database: 'inventory',
@ -107,7 +101,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',
@ -140,11 +134,5 @@ export default {
user: 'info@canpouches.com',
pass: 'WWqQ4aZq4Jrm9uwz',
},
},
upload: {
// mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
mode: 'file',
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
whitelist: ['.csv'], // 支持的文件后缀
},
}
} as MidwayConfig;

View File

@ -16,10 +16,8 @@ export default {
dataSource: {
default: {
host: 'localhost',
port: "23306",
username: 'root',
password: '12345678',
database: 'inventory',
},
},
},
@ -27,7 +25,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',
@ -35,32 +33,28 @@ export default {
},
wpSite: [
{
id: '200',
wpApiUrl: "http://simple.local",
consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886',
consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8',
siteName: 'LocalSimple',
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',
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',

View File

@ -16,7 +16,6 @@ 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';
@ -34,7 +33,6 @@ import { AuthMiddleware } from './middleware/auth.middleware';
crossDomain,
cron,
jwt,
upload,
],
importConfigs: [join(__dirname, './config')],
})

View File

@ -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('/')

View File

@ -1,98 +0,0 @@
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);
}
}
}

View File

@ -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()

View File

@ -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 {};
}

View File

@ -1,26 +0,0 @@
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);
}
}
}

View File

@ -9,10 +9,9 @@ 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, BatchUpdateProductDTO } from '../dto/product.dto';
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
import { ContentType, Files } from '@midwayjs/core';
@ -22,6 +21,7 @@ import { Context } from '@midwayjs/koa';
export class ProductController {
@Inject()
productService: ProductService;
ProductRes;
@Inject()
ctx: Context;
@ -63,14 +63,12 @@ export class ProductController {
async getProductList(
@Query() query: QueryProductDTO
): Promise<ProductListRes> {
const { current = 1, pageSize = 10, name, brandId, sortField, sortOrder } = query;
const { current = 1, pageSize = 10, name, brandId } = query;
try {
const data = await this.productService.getProductList(
{ current, pageSize },
name,
brandId,
sortField,
sortOrder
brandId
);
return successResponse(data);
} catch (error) {
@ -90,14 +88,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`;
@ -108,26 +106,15 @@ export class ProductController {
}
}
// 导入产品(CSV 文件)
// 中文注释导入产品CSV 文件)
@ApiOkResponse()
@Post('/import')
async importProductsCSV(@Files() files: any) {
try {
// 条件判断:确保存在文件
// 条件判断确保存在文件
const file = files?.[0];
if (!file?.data) return errorResponse('未接收到上传文件');
// 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);
const result = await this.productService.importProductsCSV(file.data);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || error);
@ -145,17 +132,6 @@ 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) {
@ -178,7 +154,7 @@ export class ProductController {
}
}
// 获取产品的库存组成
// 中文注释:获取产品的库存组成
@ApiOkResponse()
@Get('/:id/components')
async getProductComponents(@Param('id') id: number) {
@ -190,19 +166,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?.components || []);
const data = await this.productService.setProductComponents(id, body?.items || []);
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) {
@ -215,21 +191,7 @@ 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(
@ -250,7 +212,7 @@ export class ProductController {
}
}
// 通用属性接口:全部列表
// 通用属性接口全部列表
@ApiOkResponse()
@Get('/attributeAll')
async getAttributeAll(@Query('dictName') dictName: string) {
@ -262,7 +224,7 @@ export class ProductController {
}
}
// 通用属性接口:创建
// 通用属性接口创建
@ApiOkResponse()
@Post('/attribute')
async createAttribute(
@ -270,7 +232,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) {
@ -278,7 +240,7 @@ export class ProductController {
}
}
// 通用属性接口:更新
// 通用属性接口更新
@ApiOkResponse()
@Put('/attribute/:id')
async updateAttribute(
@ -302,7 +264,7 @@ export class ProductController {
}
}
// 通用属性接口:删除
// 通用属性接口删除
@ApiOkResponse({ type: BooleanRes })
@Del('/attribute/:id')
async deleteAttribute(@Param('id') id: number) {
@ -314,12 +276,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);
@ -330,7 +292,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);
@ -341,9 +303,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);
@ -355,10 +317,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);
@ -369,14 +331,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() {
@ -438,7 +400,7 @@ export class ProductController {
}
}
// 兼容旧接口:规格
// 兼容旧接口规格
@ApiOkResponse()
@Get('/strengthAll')
async compatStrengthAll() {
@ -500,7 +462,7 @@ export class ProductController {
}
}
// 兼容旧接口:尺寸
// 兼容旧接口尺寸
@ApiOkResponse()
@Get('/sizeAll')
async compatSizeAll() {
@ -561,88 +523,4 @@ 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);
}
}
}

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -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);

View File

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

View File

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

View File

@ -9,7 +9,7 @@ const options: DataSourceOptions & SeederOptions = {
username: 'root',
password: '12345678',
database: 'inventory',
synchronize: true,
synchronize: false,
logging: true,
entities: [__dirname + '/../entity/*.ts'],
migrations: ['src/db/migrations/**/*.ts'],

View File

@ -11,20 +11,15 @@ export default class AreaSeeder implements Seeder {
const areaRepository = dataSource.getRepository(Area);
const areas = [
{ name: 'Australia', code: 'AU' },
{ name: 'Canada', code: 'CA' },
{ name: 'United States', code: 'US' },
{ name: 'Germany', code: 'DE' },
{ name: 'Poland', code: 'PL' },
{ name: 'Australia' },
{ name: 'Canada' },
{ name: 'United States' },
{ name: 'Germany' },
{ name: 'Poland' },
];
for (const areaData of areas) {
const existingArea = await areaRepository.findOne({
where: [
{ name: areaData.name },
{ code: areaData.code }
]
});
const existingArea = await areaRepository.findOne({ where: { name: areaData.name } });
if (!existingArea) {
const newArea = areaRepository.create(areaData);
await areaRepository.save(newArea);

View File

@ -1,39 +0,0 @@
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);
}
}
}
}

View File

@ -1,62 +0,0 @@
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}`);
}
}
}
}

View File

@ -22,77 +22,79 @@ export default class DictSeeder implements Seeder {
const dictItemRepository = dataSource.getRepository(DictItem);
const flavorsData = [
{ 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: '薄荷醇' },
{ 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' },
];
const brandsData = [
{ 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: '' },
{ 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' },
];
const strengthsData = [
{ 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毫克' },
{ 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' },
];
const nonFlavorTokensData = ['slim', 'pouches', 'pouch', 'mini', 'dry'].map(item => ({ title: item, name: item }));
// 初始化语言字典
const locales = [
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文' },
{ name: 'en-us', title: 'English', titleCn: '英文' },
{ name: 'zh-cn', title: '简体中文' },
{ name: 'en-us', title: 'English' },
];
for (const locale of locales) {
@ -114,19 +116,20 @@ 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, titleCn: t.zh, dict: zhDict });
await dictItemRepository.save({ name: t.name, title: 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, titleCn: t.en, dict: enDict });
await dictItemRepository.save({ name: t.name, title: t.en, dict: enDict });
}
}
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: '强度' });
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' });
// 遍历品牌数据
await this.seedDictItems(dictItemRepository, brandDict, brandsData);
@ -136,6 +139,9 @@ export default class DictSeeder implements Seeder {
// 遍历强度数据
await this.seedDictItems(dictItemRepository, strengthDict, strengthsData);
// 遍历非口味关键词数据
await this.seedDictItems(dictItemRepository, nonFlavorTokensDict, nonFlavorTokensData);
}
/**
@ -144,13 +150,13 @@ export default class DictSeeder implements Seeder {
* @param dictInfo
* @returns Dict
*/
private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string }): Promise<Dict> {
private async createOrFindDict(repo: any, dictInfo: { title: string; name: 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({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn });
// 如果字典不存在则使用格式化后的 name 创建新字典
dict = await repo.save({ title: dictInfo.title, name: formattedName });
}
return dict;
}
@ -161,14 +167,14 @@ export default class DictSeeder implements Seeder {
* @param dict
* @param items
*/
private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string }[]): Promise<void> {
private async seedDictItems(repo: any, dict: Dict, items: { title: string; name: 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({ name: formattedName, title: item.title, titleCn: item.titleCn, dict });
// 如果字典项不存在则使用格式化后的 name 创建新字典项
await repo.save({ ...item, name: formattedName, dict });
}
}
}

View File

@ -4,14 +4,14 @@ import { Template } from '../../entity/template.entity';
/**
* @class TemplateSeeder
* @description ,.
* @description
*/
export default class TemplateSeeder implements Seeder {
/**
* @method run
* @description .,;,.
* @param {DataSource} dataSource - , repository.
* @param {SeederFactoryManager} factoryManager - Seeder .
* @description product_sku
* @param {DataSource} dataSource - repository
* @param {SeederFactoryManager} factoryManager - Seeder
*/
public async run(
dataSource: DataSource,
@ -20,38 +20,17 @@ export default class TemplateSeeder implements Seeder {
// 获取 Template 实体的 repository
const templateRepository = dataSource.getRepository(Template);
const templates = [
{
name: 'product.sku',
value: '<%= it.brand %>-<%=it.category%>-<%= it.flavor %>-<%= it.strength %>-<%= it.humidity %>',
description: '产品SKU模板',
},
{
name: 'product.title',
value: '<%= it.brand %> <%= it.flavor %> <%= it.strength %> <%= it.humidity %>',
description: '产品标题模板',
},
];
for (const t of templates) {
// 检查模板是否已存在
// 检查名为 'product_sku' 的模板是否已存在
const existingTemplate = await templateRepository.findOne({
where: { name: t.name },
where: { name: 'product_sku' },
});
if (existingTemplate) {
// 如果存在,则更新
existingTemplate.value = t.value;
existingTemplate.description = t.description;
await templateRepository.save(existingTemplate);
} else {
// 如果不存在,则创建并保存
// 如果模板不存在,则创建并保存
if (!existingTemplate) {
const template = new Template();
template.name = t.name;
template.value = t.value;
template.description = t.description;
template.name = 'product_sku';
template.value = '{{brand}}-{{flavor}}-{{strength}}-{{humidity}}';
await templateRepository.save(template);
}
}
}
}

View File

@ -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;
}

View File

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

View File

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

View File

@ -1,31 +1,6 @@
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
*/
@ -38,10 +13,6 @@ 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;
@ -50,11 +21,7 @@ export class CreateProductDTO {
@Rule(RuleType.string())
sku?: string;
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
@Rule(RuleType.number())
categoryId?: number;
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
@ApiProperty({ description: '属性列表', type: 'array' })
@Rule(RuleType.array().required())
attributes: AttributeInputDTO[];
@ -69,14 +36,12 @@ export class CreateProductDTO {
@Rule(RuleType.number())
promotionPrice?: number;
// 商品类型(默认 single; bundle 需手动设置组成)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
@Rule(RuleType.string().valid('single', 'bundle').default('single'))
// 中文注释:商品类型(默认 simplebundle 需手动设置组成)
@ApiProperty({ description: '商品类型', enum: ['simple', 'bundle'], default: 'simple', required: false })
@Rule(RuleType.string().valid('simple', 'bundle').default('simple'))
type?: string;
// 仅当 type 为 'bundle' 时,才需要提供 components
// 中文注释:仅当 type 为 'bundle' 时,才需要提供 components
@ApiProperty({ description: '产品组成', type: 'array', required: false })
@Rule(
RuleType.array()
@ -102,10 +67,6 @@ 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;
@ -114,10 +75,6 @@ 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())
@ -128,125 +85,234 @@ export class UpdateProductDTO {
@Rule(RuleType.number())
promotionPrice?: number;
// 属性更新(可选, 支持增量替换指定字典的属性项)
// 属性更新(中文注释:可选,支持增量替换指定字典的属性项)
@ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array())
attributes?: AttributeInputDTO[];
// 商品类型(single 或 bundle)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
@Rule(RuleType.string().valid('single', 'bundle'))
// 中文注释商品类型更新simple 或 bundle
@ApiProperty({ description: '商品类型', enum: ['simple', 'bundle'], required: false })
@Rule(RuleType.string().valid('simple', '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({ description: '当前页', example: 1 })
@Rule(RuleType.number().default(1))
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ description: '每页数量', example: 10 })
@Rule(RuleType.number().default(10))
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty({ description: '搜索关键字', required: false })
@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 })
@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 SetProductComponentsDTO {
@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 }[];
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[];
}

View File

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

View File

@ -38,19 +38,12 @@ 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 {
@ -61,8 +54,6 @@ 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;
@ -70,11 +61,6 @@ 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 {

View File

@ -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,11 +167,6 @@ 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 {}

View File

@ -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;
}

View File

@ -1,39 +0,0 @@
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;
}

View File

@ -1,26 +0,0 @@
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;
}

View File

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

View File

@ -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',

View File

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

View File

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

View File

@ -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;

View File

@ -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)
// 客户 IDWooCommerce 用户 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()

View File

@ -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;
}

View File

@ -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();
// 控制器之后执行的逻辑

View File

@ -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',

View File

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

View File

@ -1,105 +0,0 @@
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);
}
}

View File

@ -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 [];
}

View File

@ -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);

View File

@ -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

View File

@ -3,8 +3,7 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository, Like, In } from 'typeorm';
import { Site } from '../entity/site.entity';
import { WpSite } from '../interface';
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
import { Area } from '../entity/area.entity';
import { UpdateSiteDTO } from '../dto/site.dto';
@Provide()
@Scope(ScopeEnum.Singleton)
@ -12,16 +11,11 @@ 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,
@ -30,143 +24,64 @@ 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: 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);
async create(data: Partial<Site>) {
// 创建新的站点记录
await this.siteModel.insert(data as Site);
return true;
}
async update(id: string | number, data: UpdateSiteDTO) {
// 从 DTO 中分离出区域代码和其他站点数据
const { areas: areaCodes, ...restData } = data;
// 首先,根据 ID 查找要更新的站点实体
const siteToUpdate = await this.siteModel.findOne({
where: { id: Number(id) },
});
if (!siteToUpdate) {
// 如果找不到站点,则操作失败
return false;
}
// 更新站点的基本字段
// 更新指定站点记录,将布尔 isDisabled 转换为数值 0/1
const payload: Partial<Site> = {
...restData,
...data,
isDisabled:
data.isDisabled === undefined
data.isDisabled === undefined // 未传入则不更新该字段
? undefined
: data.isDisabled
: data.isDisabled // true -> 1, false -> 0
? 1
: 0,
} as any;
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);
await this.siteModel.update({ id: Number(id) }, payload);
return true;
}
async get(id: string | number, includeSecret = false) {
// 根据主键获取站点,并使用 relations 加载关联的 areas
const site = await this.siteModel.findOne({
where: { id: Number(id) },
relations: ['areas'],
});
if (!site) {
return null;
}
// 如果需要包含密钥,则直接返回
if (includeSecret) {
return site;
}
// 默认不返回密钥,进行字段脱敏
// 根据主键获取站点includeSecret 为 true 时返回密钥字段
const site = await this.siteModel.findOne({ where: { id: Number(id) } });
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);
}
}
// 进行分页查询,并使用 relations 加载关联的 areas
const [items, total] = await this.siteModel.findAndCount({
where,
skip: (current - 1) * pageSize,
take: pageSize,
relations: ['areas'],
});
// 进行分页查询skip/take并返回总条数
const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize });
// 根据 includeSecret 决定是否脱敏返回密钥字段
const data = includeSecret
? items
: items.map((item: any) => {
const data = includeSecret ? items : items.map((item: any) => {
const { consumerKey, consumerSecret, ...rest } = item;
return rest;
});
@ -174,7 +89,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;
}

View File

@ -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,

View File

@ -1,5 +1,5 @@
import { Provide } from '@midwayjs/core';
import { Between, Like, Repository, LessThan, MoreThan, In } from 'typeorm';
import { Between, Like, Repository, LessThan, MoreThan } from 'typeorm';
import { Stock } from '../entity/stock.entity';
import { StockRecord } from '../entity/stock_record.entity';
import { paginate } from '../utils/paginate.util';
@ -27,7 +27,6 @@ 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 {
@ -52,55 +51,35 @@ export class StockService {
@InjectEntityModel(TransferItem)
transferItemModel: Repository<TransferItem>;
@InjectEntityModel(Area)
areaModel: Repository<Area>;
async createStockPoint(data: CreateStockPointDTO) {
const { areas: areaCodes, ...restData } = data;
const { name, location, contactPerson, contactPhone } = data;
const stockPoint = new StockPoint();
Object.assign(stockPoint, restData);
if (areaCodes && areaCodes.length > 0) {
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
stockPoint.areas = areas;
} else {
stockPoint.areas = [];
}
stockPoint.name = name;
stockPoint.location = location;
stockPoint.contactPerson = contactPerson;
stockPoint.contactPhone = contactPhone;
await this.stockPointModel.save(stockPoint);
}
async updateStockPoint(id: number, data: UpdateStockPointDTO) {
const { areas: areaCodes, ...restData } = data;
const pointToUpdate = await this.stockPointModel.findOneBy({ id });
if (!pointToUpdate) {
throw new Error(`仓库点 ID ${id} 不存在`);
// 确认产品是否存在
const point = await this.stockPointModel.findOneBy({ id });
if (!point) {
throw new Error(`产品 ID ${id} 不存在`);
}
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);
// 更新产品
await this.stockPointModel.update(id, data);
}
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({ relations: ['areas'] });
return await this.stockPointModel.find();
}
async delStockPoints(id: number) {
@ -139,7 +118,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;
@ -208,7 +187,7 @@ export class StockService {
);
}
// 检查指定 SKU 是否在任一仓库有库存(数量大于 0)
// 中文注释:检查指定 SKU 是否在任一仓库有库存(数量大于 0
async hasStockBySku(sku: string): Promise<boolean> {
const count = await this.stockModel
.createQueryBuilder('stock')
@ -222,7 +201,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 });
}
@ -231,7 +210,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 },
});
@ -403,7 +382,7 @@ export class StockService {
sku,
});
if (!stock) {
// 如果库存不存在,则直接新增
// 如果库存不存在则直接新增
const newStock = this.stockModel.create({
stockPointId,
sku,
@ -415,7 +394,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);
}
@ -571,7 +550,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 },
});
@ -594,7 +573,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 },
});
@ -617,7 +596,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);
}

View File

@ -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,

View File

@ -3,15 +3,12 @@ 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>;
@ -67,7 +64,7 @@ export class TemplateService {
): Promise<Template> {
// 首先根据 ID 查找模板
const template = await this.templateModel.findOneBy({ id });
// 如果模板不存在,则抛出错误
// 如果模板不存在则抛出错误
if (!template) {
throw new Error(`模板 ID ${id} 不存在`);
}
@ -81,22 +78,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;
}
@ -109,12 +106,22 @@ 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}' 不存在`);
}
// 使用 Eta 渲染
return this.eta.renderString(template.value, data);
// 获取模板的原始内容
let rendered = template.value;
// 遍历数据对象,替换模板中的占位符
for (const key in data) {
// 创建一个正则表达式来匹配 {{key}}
const regex = new RegExp(`{{${key}}}`, 'g');
// 执行替换操作
rendered = rendered.replace(regex, data[key]);
}
// 返回渲染后的字符串
return rendered;
}
}

View File

@ -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`,

View File

@ -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;
// 保存更新

View File

@ -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/subscriptionsSubscriptions 退 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,37 +403,4 @@ 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
};
}
}

View File

@ -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}`;

View File

@ -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 {

View File

@ -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"]