diff --git a/.gitignore b/.gitignore index cd12d03..d3b97ca 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ run/ yarn.lock **/config.prod.ts **/config.local.ts -container \ No newline at end of file +container +ai/products-20251202 (1).csv diff --git a/area-api-doc.md b/area-api-doc.md index 8eb579d..6dd8f8e 100644 --- a/area-api-doc.md +++ b/area-api-doc.md @@ -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可能返回的错误信息: - `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时 - `区域不存在`: 当尝试更新或删除不存在的区域时 diff --git a/migration-guide.md b/migration-guide.md index ba5339e..4bb0666 100644 --- a/migration-guide.md +++ b/migration-guide.md @@ -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开始为区域添加坐标信息 \ No newline at end of file +- 迁移不会影响现有数据,新增字段默认为 NULL +- 迁移后,可以通过API开始为区域添加坐标信息 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 529b58c..379b738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1f27fdc..46d1f1d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/replace_punctuation.js b/replace_punctuation.js new file mode 100644 index 0000000..5149513 --- /dev/null +++ b/replace_punctuation.js @@ -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}`); diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 1eb06cf..b00d992 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -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; diff --git a/src/configuration.ts b/src/configuration.ts index e413b12..09019e4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -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')], }) diff --git a/src/controller/area.controller.ts b/src/controller/area.controller.ts index 88304e5..4c28193 100644 --- a/src/controller/area.controller.ts +++ b/src/controller/area.controller.ts @@ -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('/') diff --git a/src/controller/dict.controller.ts b/src/controller/dict.controller.ts index bb4cc91..8834267 100644 --- a/src/controller/dict.controller.ts +++ b/src/controller/dict.controller.ts @@ -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() diff --git a/src/controller/locale.controller.ts b/src/controller/locale.controller.ts index e1910ad..3377512 100644 --- a/src/controller/locale.controller.ts +++ b/src/controller/locale.controller.ts @@ -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 {}; } diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index b3e3280..c905ee6 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -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 { - 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() { diff --git a/src/controller/stock.controller.ts b/src/controller/stock.controller.ts index b3417ec..1728bda 100644 --- a/src/controller/stock.controller.ts +++ b/src/controller/stock.controller.ts @@ -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) { diff --git a/src/controller/subscription.controller.ts b/src/controller/subscription.controller.ts index 080c799..c9e4537 100644 --- a/src/controller/subscription.controller.ts +++ b/src/controller/subscription.controller.ts @@ -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) { diff --git a/src/controller/template.controller.ts b/src/controller/template.controller.ts index 80d0867..6d009e3 100644 --- a/src/controller/template.controller.ts +++ b/src/controller/template.controller.ts @@ -47,7 +47,7 @@ export class TemplateController { /** * @summary 创建新模板 - * @description 创建一个新的模板,用于后续的字符串生成 + * @description 创建一个新的模板,用于后续的字符串生成 * @param templateData 模板数据 */ @ApiOkResponse({ type: Template, description: '成功创建模板' }) diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index a6fc02e..2c583fe 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -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); diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index bb254c7..a9dca7d 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -33,7 +33,7 @@ export class WebhookController { @Inject() private readonly siteService: SiteService; - // 移除配置中的站点数组,来源统一改为数据库 + // 移除配置中的站点数组,来源统一改为数据库 @Get('/') async test() { diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts index 02a644e..f945a6d 100644 --- a/src/controller/wp_product.controller.ts +++ b/src/controller/wp_product.controller.ts @@ -25,7 +25,7 @@ import { } from '../dto/reponse.dto'; @Controller('/wp_product') export class WpProductController { - // 移除控制器内的配置站点引用,统一由服务层处理站点数据 + // 移除控制器内的配置站点引用,统一由服务层处理站点数据 @Inject() private readonly wpProductService: WpProductService; diff --git a/src/db/seeds/dict.seeder.ts b/src/db/seeds/dict.seeder.ts index 35610b8..808659e 100644 --- a/src/db/seeds/dict.seeder.ts +++ b/src/db/seeds/dict.seeder.ts @@ -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 }); } } diff --git a/src/db/seeds/template.seeder.ts b/src/db/seeds/template.seeder.ts index f7ce0bc..593a579 100644 --- a/src/db/seeds/template.seeder.ts +++ b/src/db/seeds/template.seeder.ts @@ -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'; diff --git a/src/dto/area.dto.ts b/src/dto/area.dto.ts index 9004e30..2523286 100644 --- a/src/dto/area.dto.ts +++ b/src/dto/area.dto.ts @@ -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; } diff --git a/src/dto/freightcom.dto.ts b/src/dto/freightcom.dto.ts index 2bba475..32b998d 100644 --- a/src/dto/freightcom.dto.ts +++ b/src/dto/freightcom.dto.ts @@ -8,7 +8,7 @@ export type PackagingType = // | PackagingCourierPak // | PackagingEnvelope; -// 定义包装类型的枚举,用于 API 文档描述 +// 定义包装类型的枚举,用于 API 文档描述 export enum PackagingTypeEnum { Pallet = 'pallet', Package = 'package', diff --git a/src/dto/order.dto.ts b/src/dto/order.dto.ts index 26a1b73..34e35db 100644 --- a/src/dto/order.dto.ts +++ b/src/dto/order.dto.ts @@ -92,7 +92,7 @@ export class QueryOrderDTO { @Rule(RuleType.string()) payment_method: string; - @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' }) + @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' }) @Rule(RuleType.bool().default(false)) isSubscriptionOnly?: boolean; } diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index 6a9b108..38eeccd 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -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; - // 中文注释:商品类型(默认 single;bundle 需手动设置组成) + + + // 商品类型(默认 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; } /** diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts index 1c43649..6180703 100644 --- a/src/dto/reponse.dto.ts +++ b/src/dto/reponse.dto.ts @@ -148,7 +148,7 @@ export class PaymentMethodListRes extends SuccessArrayWrapper( PaymentMethodDTO ) {} -// 订阅分页数据(列表 + 总数等分页信息) +// 订阅分页数据(列表 + 总数等分页信息) export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {} -// 订阅分页返回数据(统一成功包装) +// 订阅分页返回数据(统一成功包装) export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {} diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index 58f3e63..103c9ad 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -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; diff --git a/src/dto/stock.dto.ts b/src/dto/stock.dto.ts index 639ea6d..aa5a013 100644 --- a/src/dto/stock.dto.ts +++ b/src/dto/stock.dto.ts @@ -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; } diff --git a/src/dto/subscription.dto.ts b/src/dto/subscription.dto.ts index 79794db..4276ec1 100644 --- a/src/dto/subscription.dto.ts +++ b/src/dto/subscription.dto.ts @@ -2,9 +2,9 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Rule, RuleType } from '@midwayjs/validate'; import { SubscriptionStatus } from '../enums/base.enum'; -// 订阅列表查询参数(分页与筛选) +// 订阅列表查询参数(分页与筛选) export class QuerySubscriptionDTO { - // 当前页码(从 1 开始) + // 当前页码(从 1 开始) @ApiProperty({ example: 1, description: '页码' }) @Rule(RuleType.number().default(1)) current: number; @@ -14,23 +14,23 @@ export class QuerySubscriptionDTO { @Rule(RuleType.number().default(10)) pageSize: number; - // 站点 ID(可选) + // 站点 ID(可选) @ApiProperty({ description: '站点ID' }) @Rule(RuleType.string().allow('')) siteId: string; - // 订阅状态筛选(可选),支持枚举值 + // 订阅状态筛选(可选),支持枚举值 @ApiProperty({ description: '订阅状态', enum: SubscriptionStatus }) @Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow('')) status: SubscriptionStatus | ''; - // 客户邮箱(模糊匹配,可选) + // 客户邮箱(模糊匹配,可选) @ApiProperty({ description: '客户邮箱' }) @Rule(RuleType.string().allow('')) customer_email: string; - // 关键字(订阅ID、邮箱等,模糊匹配,可选) - @ApiProperty({ description: '关键字(订阅ID、邮箱等)' }) + // 关键字(订阅ID,邮箱等,模糊匹配,可选) + @ApiProperty({ description: '关键字(订阅ID,邮箱等)' }) @Rule(RuleType.string().allow('')) keyword: string; } \ No newline at end of file diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts index 4c7d293..09fd126 100644 --- a/src/entity/order.entity.ts +++ b/src/entity/order.entity.ts @@ -178,7 +178,7 @@ export class Order { @ApiProperty() @Column({ type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT - nullable: true, // 可选:是否允许为 NULL + nullable: true, // 可选:是否允许为 NULL }) @Expose() customer_note: string; diff --git a/src/entity/order_item.entity.ts b/src/entity/order_item.entity.ts index 4a97b0a..dca5c62 100644 --- a/src/entity/order_item.entity.ts +++ b/src/entity/order_item.entity.ts @@ -79,17 +79,17 @@ export class OrderItem { @ApiProperty() @Column({ nullable: true }) @Expose() - tax_class?: string; // 税类(来自 line_items.tax_class) + tax_class?: string; // 税类(来自 line_items.tax_class) @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() - taxes?: any[]; // 税明细(来自 line_items.taxes,数组) + taxes?: any[]; // 税明细(来自 line_items.taxes,数组) @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() - meta_data?: any[]; // 行项目元数据(包含订阅相关键值) + meta_data?: any[]; // 行项目元数据(包含订阅相关键值) @ApiProperty() @Column({ nullable: true }) @@ -99,7 +99,7 @@ export class OrderItem { @ApiProperty() @Column({ nullable: true }) @Expose() - global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供) + global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供) @ApiProperty() @Column('decimal', { precision: 10, scale: 2 }) @@ -109,17 +109,17 @@ export class OrderItem { @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() - image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src) + image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src) @ApiProperty() @Column({ nullable: true }) @Expose() - parent_name?: string; // 父商品名称(组合/捆绑时可能使用) + parent_name?: string; // 父商品名称(组合/捆绑时可能使用) @ApiProperty() @Column({ nullable: true }) @Expose() - bundled_by?: string; // 捆绑来源标识(bundled_by) + bundled_by?: string; // 捆绑来源标识(bundled_by) @ApiProperty() @Column({ nullable: true }) @@ -129,7 +129,7 @@ export class OrderItem { @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() - bundled_items?: any[]; // 捆绑项列表(数组) + bundled_items?: any[]; // 捆绑项列表(数组) @ApiProperty({ example: '2022-12-12 11:11:11', diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index 2d26865..f3702ca 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -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[]; diff --git a/src/entity/product_stock_component.entity.ts b/src/entity/product_stock_component.entity.ts index 6e0cbb3..4048070 100644 --- a/src/entity/product_stock_component.entity.ts +++ b/src/entity/product_stock_component.entity.ts @@ -20,7 +20,7 @@ export class ProductStockComponent { @Column({ type: 'int', default: 1 }) quantity: number; - // 中文注释:多对一,组件隶属于一个产品 + // 多对一,组件隶属于一个产品 @ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' }) product: Product; diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts index 87b452d..cdf70d9 100644 --- a/src/entity/site.entity.ts +++ b/src/entity/site.entity.ts @@ -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; diff --git a/src/entity/subscription.entity.ts b/src/entity/subscription.entity.ts index 10d205f..8ec28e2 100644 --- a/src/entity/subscription.entity.ts +++ b/src/entity/subscription.entity.ts @@ -12,68 +12,68 @@ import { SubscriptionStatus } from '../enums/base.enum'; @Entity('subscription') @Exclude() export class Subscription { - // 本地主键,自增 ID + // 本地主键,自增 ID @ApiProperty() @PrimaryGeneratedColumn() @Expose() id: number; - // 站点唯一标识,用于区分不同来源站点 + // 站点唯一标识,用于区分不同来源站点 @ApiProperty({ description: '来源站点唯一标识' }) @Column({ nullable: true }) @Expose() siteId: number; - // WooCommerce 订阅的原始 ID(字符串化),用于幂等更新 + // WooCommerce 订阅的原始 ID(字符串化),用于幂等更新 @ApiProperty({ description: 'WooCommerce 订阅 ID' }) @Column() @Expose() externalSubscriptionId: string; - // 订阅状态(active/cancelled/on-hold 等) + // 订阅状态(active/cancelled/on-hold 等) @ApiProperty({ type: SubscriptionStatus }) @Column({ type: 'enum', enum: SubscriptionStatus }) @Expose() status: SubscriptionStatus; - // 货币代码,例如 USD/CAD + // 货币代码,例如 USD/CAD @ApiProperty() @Column({ default: '' }) @Expose() currency: string; - // 总金额,保留两位小数 + // 总金额,保留两位小数 @ApiProperty() @Column('decimal', { precision: 10, scale: 2, default: 0 }) @Expose() total: number; - // 计费周期(day/week/month/year) + // 计费周期(day/week/month/year) @ApiProperty({ description: '计费周期 e.g. day/week/month/year' }) @Column({ default: '' }) @Expose() billing_period: string; - // 计费周期间隔(例如 1/3/12) + // 计费周期间隔(例如 1/3/12) @ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' }) @Column({ type: 'int', default: 0 }) @Expose() billing_interval: number; - // 客户 ID(WooCommerce 用户 ID) + // 客户 ID(WooCommerce 用户 ID) @ApiProperty() @Column({ type: 'int', default: 0 }) @Expose() customer_id: number; - // 客户邮箱(从 billing.email 或 customer_email 提取) + // 客户邮箱(从 billing.email 或 customer_email 提取) @ApiProperty() @Column({ default: '' }) @Expose() customer_email: string; - // 父订单/订阅 ID(如有) - @ApiProperty({ description: '父订单/父订阅ID(如有)' }) + // 父订单/订阅 ID(如有) + @ApiProperty({ description: '父订单/父订阅ID(如有)' }) @Column({ type: 'int', default: 0 }) @Expose() parent_id: number; @@ -102,25 +102,25 @@ export class Subscription { @Expose() end_date: Date; - // 商品项(订阅行项目) + // 商品项(订阅行项目) @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() line_items: any[]; - // 额外元数据(键值对) + // 额外元数据(键值对) @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() meta_data: any[]; - // 创建时间(数据库自动生成) + // 创建时间(数据库自动生成) @ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true }) @CreateDateColumn() @Expose() createdAt: Date; - // 更新时间(数据库自动生成) + // 更新时间(数据库自动生成) @ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true }) @UpdateDateColumn() @Expose() diff --git a/src/entity/user.entity.ts b/src/entity/user.entity.ts index c538601..9387610 100644 --- a/src/entity/user.entity.ts +++ b/src/entity/user.entity.ts @@ -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; } diff --git a/src/middleware/report.middleware.ts b/src/middleware/report.middleware.ts index 8121353..86e6d54 100644 --- a/src/middleware/report.middleware.ts +++ b/src/middleware/report.middleware.ts @@ -7,7 +7,7 @@ export class ReportMiddleware implements IMiddleware { return async (ctx: Context, next: NextFunction) => { // 控制器前执行的逻辑 const startTime = Date.now(); - // 执行下一个 Web 中间件,最后执行到控制器 + // 执行下一个 Web 中间件,最后执行到控制器 // 这里可以拿到下一个中间件或者控制器的返回值 const result = await next(); // 控制器之后执行的逻辑 diff --git a/src/service/area.service.ts b/src/service/area.service.ts index 1ac8bf5..e8225d5 100644 --- a/src/service/area.service.ts +++ b/src/service/area.service.ts @@ -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', diff --git a/src/service/canadaPost.service.ts b/src/service/canadaPost.service.ts index 0edee31..e4aa6ec 100644 --- a/src/service/canadaPost.service.ts +++ b/src/service/canadaPost.service.ts @@ -57,7 +57,7 @@ export class CanadaPostService { return builder.buildObject(xmlObj); } - // 默认直接构建(用于 createShipment 这类已有完整结构) + // 默认直接构建(用于 createShipment 这类已有完整结构) return builder.buildObject(data); } diff --git a/src/service/dict.service.ts b/src/service/dict.service.ts index 1169287..6bce09a 100644 --- a/src/service/dict.service.ts +++ b/src/service/dict.service.ts @@ -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 []; } diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index a4f2b9d..067f4e5 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -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); diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 0ad083c..3020542 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -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) { // 从数据中解构出需要用的属性 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}`); } } diff --git a/src/service/product.service.ts b/src/service/product.service.ts index c156cf3..be17eb0 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -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 = {}; 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 { 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 { - const { name, description, attributes, sku, price, categoryId } = createProductDTO; - // 条件判断(中文注释:校验属性输入) + async createProduct(createProductDTO: CreateProductDTO): Promise { + 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 { - // 检查产品是否存在(包含属性关系) + // 检查产品是否存在(包含属性关系) 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 { - // 条件判断:确保产品存在 + // 条件判断:确保产品存在 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 { - // 条件判断:确保产品存在 + // 条件判断:确保产品存在 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 { - // 条件判断:确保产品存在 + // 条件判断:确保产品存在 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 { // 确认产品是否存在 @@ -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 { - // 查找 '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 { - // 查找 '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 { 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 { - // 先查询(中文注释:确保尺寸项存在) + // 先查询(确保尺寸项存在) 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 { 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 { 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 { - // 查询所有产品及其属性(中文注释:包含字典关系) - 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(); + 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 { + // 查询所有产品及其属性(包含字典关系)和组成 + const products = await this.productModel.find({ + relations: ['attributes', 'attributes.dict', 'components'], + order: { id: 'ASC' }, + }); + + // 1. 收集所有动态属性的 dictName + const dictNames = new Set(); + // 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)); } diff --git a/src/service/site.service.ts b/src/service/site.service.ts index 6ae7fbd..ffc7b0c 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -16,7 +16,7 @@ export class SiteService { areaModel: Repository; 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; } diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index f7a026c..71804ce 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -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, diff --git a/src/service/stock.service.ts b/src/service/stock.service.ts index 2adc686..bc7d4e1 100644 --- a/src/service/stock.service.ts +++ b/src/service/stock.service.ts @@ -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 { 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); } diff --git a/src/service/subscription.service.ts b/src/service/subscription.service.ts index 90d344a..4d03884 100644 --- a/src/service/subscription.service.ts +++ b/src/service/subscription.service.ts @@ -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, diff --git a/src/service/template.service.ts b/src/service/template.service.ts index aa43771..45f5559 100644 --- a/src/service/template.service.ts +++ b/src/service/template.service.ts @@ -64,7 +64,7 @@ export class TemplateService { ): Promise