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

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

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

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

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

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

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

3
.gitignore vendored
View File

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

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

164
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@midwayjs/logger": "^3.1.0",
"@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0",
"@midwayjs/upload": "^3.20.16",
"@midwayjs/validate": "^3.20.2",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"axios": "^1.13.2",
@ -813,6 +814,19 @@
"node": ">=12"
}
},
"node_modules/@midwayjs/upload": {
"version": "3.20.16",
"resolved": "https://registry.npmjs.org/@midwayjs/upload/-/upload-3.20.16.tgz",
"integrity": "sha512-lnHDOeU4wGvABJjYjSR5dpG+6f4+hxJhU+1TsT+OsrKMDju6AyTOnTSFa1V1vMO7+VplKzkWP+ZFJkAVI0Buuw==",
"license": "MIT",
"dependencies": {
"file-type": "16.5.4",
"raw-body": "2.5.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@midwayjs/validate": {
"version": "3.20.13",
"resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.13.tgz",
@ -928,6 +942,12 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
"license": "MIT"
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
"node_modules/@types/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmmirror.com/@types/accepts/-/accepts-1.3.7.tgz",
@ -1153,6 +1173,18 @@
"node": ">=8.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@ -2246,6 +2278,24 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
@ -2278,6 +2328,23 @@
"reusify": "^1.0.4"
}
},
"node_modules/file-type": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
"license": "MIT",
"dependencies": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
"token-types": "^4.1.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
@ -3607,6 +3674,19 @@
"node": ">=8"
}
},
"node_modules/peek-readable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
@ -3641,6 +3721,15 @@
"node": ">= 0.4"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -3777,6 +3866,47 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/readable-web-to-node-stream": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
"integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
"license": "MIT",
"dependencies": {
"readable-stream": "^4.7.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/readable-web-to-node-stream/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/readable-web-to-node-stream/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@ -4302,6 +4432,23 @@
"node": ">=8"
}
},
"node_modules/strtok3": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/superagent": {
"version": "8.1.2",
"resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
@ -4402,6 +4549,23 @@
"node": ">=0.6"
}
},
"node_modules/token-types": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tsc-alias": {
"version": "1.8.16",
"resolved": "https://registry.npmmirror.com/tsc-alias/-/tsc-alias-1.8.16.tgz",

View File

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

83
replace_punctuation.js Normal file
View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

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

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

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,6 +38,8 @@ 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())
@ -59,6 +61,8 @@ export class UpdateSiteDTO {
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string().optional())
token?: string;
@Rule(RuleType.string().optional())
name?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;

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

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

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

@ -63,9 +63,7 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number;
@ApiProperty({ description: '库存', example: 100 })
@Column({ default: 0 })
stock: number;
// 分类关联
@ -79,7 +77,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;
// 客户 IDWooCommerce 用户 ID
// 客户 ID(WooCommerce 用户 ID)
@ApiProperty()
@Column({ type: 'int', default: 0 })
@Expose()
customer_id: number;
// 客户邮箱(从 billing.email 或 customer_email 提取)
// 客户邮箱(从 billing.email 或 customer_email 提取)
@ApiProperty()
@Column({ default: '' })
@Expose()
customer_email: string;
// 父订单/订阅 ID(如有)
@ApiProperty({ description: '父订单/父订阅ID(如有)' })
// 父订单/订阅 ID(如有)
@ApiProperty({ description: '父订单/父订阅ID(如有)' })
@Column({ type: 'int', default: 0 })
@Expose()
parent_id: number;
@ -102,25 +102,25 @@ export class Subscription {
@Expose()
end_date: Date;
// 商品项(订阅行项目)
// 商品项(订阅行项目)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
line_items: any[];
// 额外元数据(键值对)
// 额外元数据(键值对)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: any[];
// 创建时间(数据库自动生成)
// 创建时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
@CreateDateColumn()
@Expose()
createdAt: Date;
// 更新时间(数据库自动生成)
// 更新时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
@UpdateDateColumn()
@Expose()

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

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

View File

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

View File

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

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

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

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

@ -64,7 +64,7 @@ export class TemplateService {
): Promise<Template> {
// 首先根据 ID 查找模板
const template = await this.templateModel.findOneBy({ id });
// 如果模板不存在则抛出错误
// 如果模板不存在,则抛出错误
if (!template) {
throw new Error(`模板 ID ${id} 不存在`);
}
@ -78,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;
}
@ -106,14 +106,14 @@ export class TemplateService {
async render(name: string, data: Record<string, any>): Promise<string> {
// 根据名称获取模板
const template = await this.getTemplateByName(name);
// 如果模板不存在则抛出错误
// 如果模板不存在,则抛出错误
if (!template) {
throw new Error(`模板 '${name}' 不存在`);
}
// 获取模板的原始内容
let rendered = template.value;
// 遍历数据对象替换模板中的占位符
// 遍历数据对象,替换模板中的占位符
for (const key in data) {
// 创建一个正则表达式来匹配 {{key}}
const regex = new RegExp(`{{${key}}}`, 'g');

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/subscriptionsSubscriptions 退 wc/v3/subscriptions
*
* wc/v1/subscriptions(Subscriptions ),退 wc/v3/subscriptions.
* .
*/
async getSubscriptions(siteId: number): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId);
// 优先使用 Subscriptions 命名空间 wcs/v1失败回退 wc/v3
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions');
@ -257,7 +257,7 @@ export class WPService {
);
const config: AxiosRequestConfig = {
method: 'PUT',
// 构建 URL规避多/或少/问题
// 构建 URL,规避多/或少/问题
url: this.buildURL(apiUrl, '/wp-json', endpoint),
headers: {
Authorization: `Basic ${auth}`,
@ -304,7 +304,7 @@ export class WPService {
): Promise<Boolean> {
const res = await this.updateData(`/wc/v3/products/${productId}`, site, {
status,
manage_stock: false, // 为true的时候用quantity控制库存为false时直接用stock_status控制
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
stock_status,
});
return res;
@ -357,7 +357,7 @@ export class WPService {
);
const config: AxiosRequestConfig = {
method: 'POST',
// 构建 URL规避多/或少/问题
// 构建 URL,规避多/或少/问题
url: this.buildURL(
apiUrl,
'/wp-json',
@ -385,10 +385,10 @@ export class WPService {
);
console.log('del', orderId, trackingId);
// 删除接口 DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
// 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
const config: AxiosRequestConfig = {
method: 'DELETE',
// 构建 URL规避多/或少/问题
// 构建 URL,规避多/或少/问题
url: this.buildURL(
apiUrl,
'/wp-json',

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