Compare commits
8 Commits
fb8509b5f5
...
58004dd091
| Author | SHA1 | Date |
|---|---|---|
|
|
58004dd091 | |
|
|
7b3c7540d7 | |
|
|
6855b13ed7 | |
|
|
fdf2819b3b | |
|
|
a7d5db33f3 | |
|
|
64b8468df8 | |
|
|
bc575840b2 | |
|
|
366fd93dde |
|
|
@ -0,0 +1,254 @@
|
||||||
|
# Area 区域管理 API 文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能。
|
||||||
|
|
||||||
|
## API 接口列表
|
||||||
|
|
||||||
|
### 1. 创建区域
|
||||||
|
|
||||||
|
**请求信息**
|
||||||
|
- URL: `/api/area`
|
||||||
|
- Method: `POST`
|
||||||
|
- Headers: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
**请求体 (JSON)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "欧洲",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**
|
||||||
|
- `name`: 区域名称 (必填)
|
||||||
|
- `latitude`: 纬度 (-90 到 90 之间,可选)
|
||||||
|
- `longitude`: 经度 (-180 到 180 之间,可选)
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "创建成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "欧洲",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522,
|
||||||
|
"createdAt": "2024-01-01T12:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 更新区域
|
||||||
|
|
||||||
|
**请求信息**
|
||||||
|
- URL: `/api/area/{id}`
|
||||||
|
- Method: `PUT`
|
||||||
|
- Headers: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
**请求体 (JSON)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "欧洲区域",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**
|
||||||
|
- `name`: 区域名称 (可选)
|
||||||
|
- `latitude`: 纬度 (-90 到 90 之间,可选)
|
||||||
|
- `longitude`: 经度 (-180 到 180 之间,可选)
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "更新成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "欧洲区域",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522,
|
||||||
|
"createdAt": "2024-01-01T12:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T12:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 删除区域
|
||||||
|
|
||||||
|
**请求信息**
|
||||||
|
- URL: `/api/area/{id}`
|
||||||
|
- Method: `DELETE`
|
||||||
|
- Headers: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "删除成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 获取区域列表(分页)
|
||||||
|
|
||||||
|
**请求信息**
|
||||||
|
- URL: `/api/area`
|
||||||
|
- Method: `GET`
|
||||||
|
- Headers: `Authorization: Bearer {token}`
|
||||||
|
- Query Parameters:
|
||||||
|
- `currentPage`: 当前页码 (默认 1)
|
||||||
|
- `pageSize`: 每页数量 (默认 10)
|
||||||
|
- `name`: 区域名称(可选,用于搜索)
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "查询成功",
|
||||||
|
"data": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "欧洲",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522,
|
||||||
|
"createdAt": "2024-01-01T12:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T12:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 获取所有区域
|
||||||
|
|
||||||
|
**请求信息**
|
||||||
|
- URL: `/api/area/all`
|
||||||
|
- Method: `GET`
|
||||||
|
- Headers: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "查询成功",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "欧洲",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522,
|
||||||
|
"createdAt": "2024-01-01T12:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T12:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "亚洲",
|
||||||
|
"latitude": 35.6762,
|
||||||
|
"longitude": 139.6503,
|
||||||
|
"createdAt": "2024-01-01T12:10:00Z",
|
||||||
|
"updatedAt": "2024-01-01T12:10:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 根据ID获取区域详情
|
||||||
|
|
||||||
|
**请求信息**
|
||||||
|
- URL: `/api/area/{id}`
|
||||||
|
- Method: `GET`
|
||||||
|
- Headers: `Authorization: Bearer {token}`
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "查询成功",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "欧洲",
|
||||||
|
"latitude": 48.8566,
|
||||||
|
"longitude": 2.3522,
|
||||||
|
"createdAt": "2024-01-01T12:00:00Z",
|
||||||
|
"updatedAt": "2024-01-01T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 世界地图实现建议
|
||||||
|
|
||||||
|
对于前端实现世界地图并显示区域坐标,推荐以下方案:
|
||||||
|
|
||||||
|
### 1. 使用开源地图库
|
||||||
|
|
||||||
|
- **Leaflet.js**: 轻量级开源地图库,易于集成
|
||||||
|
- **Mapbox**: 提供丰富的地图样式和交互功能
|
||||||
|
- **Google Maps API**: 功能强大但需要API密钥
|
||||||
|
|
||||||
|
### 2. 实现步骤
|
||||||
|
|
||||||
|
1. **获取区域数据**:
|
||||||
|
使用 `/api/area/all` 接口获取所有包含坐标信息的区域
|
||||||
|
|
||||||
|
2. **初始化地图**:
|
||||||
|
```javascript
|
||||||
|
// Leaflet示例
|
||||||
|
const map = L.map('map').setView([0, 0], 2);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **添加区域标记**:
|
||||||
|
```javascript
|
||||||
|
// 假设areas是从API获取的数据
|
||||||
|
areas.forEach(area => {
|
||||||
|
if (area.latitude && area.longitude) {
|
||||||
|
const marker = L.marker([area.latitude, area.longitude]).addTo(map);
|
||||||
|
marker.bindPopup(`<b>${area.name}</b>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **添加交互功能**:
|
||||||
|
- 点击标记显示区域详情
|
||||||
|
- 搜索和筛选功能
|
||||||
|
- 编辑坐标功能(调用更新API)
|
||||||
|
|
||||||
|
### 3. 坐标输入建议
|
||||||
|
|
||||||
|
在区域管理界面,可以添加以下功能来辅助坐标输入:
|
||||||
|
|
||||||
|
1. 提供搜索框,根据地点名称自动获取坐标
|
||||||
|
2. 集成小型地图,允许用户点击选择位置
|
||||||
|
3. 添加验证,确保输入的坐标在有效范围内
|
||||||
|
|
||||||
|
## 数据模型说明
|
||||||
|
|
||||||
|
### Area 实体
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 描述 | 是否必填 |
|
||||||
|
|--------|------|------|----------|
|
||||||
|
| id | number | 区域ID | 否(自动生成) |
|
||||||
|
| name | string | 区域名称 | 是 |
|
||||||
|
| latitude | number | 纬度(范围:-90 到 90) | 否 |
|
||||||
|
| longitude | number | 经度(范围:-180 到 180) | 否 |
|
||||||
|
| createdAt | Date | 创建时间 | 否(自动生成) |
|
||||||
|
| updatedAt | Date | 更新时间 | 否(自动生成) |
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
API可能返回的错误信息:
|
||||||
|
|
||||||
|
- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
|
||||||
|
- `区域不存在`: 当尝试更新或删除不存在的区域时
|
||||||
|
- `权限错误`: 当请求缺少有效的授权令牌时
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# 数据库迁移指南
|
||||||
|
|
||||||
|
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude` 和 `longitude` 字段添加到数据库表中。
|
||||||
|
|
||||||
|
## 执行迁移步骤
|
||||||
|
|
||||||
|
### 1. 生成迁移文件
|
||||||
|
|
||||||
|
运行以下命令生成迁移文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 检查迁移文件
|
||||||
|
|
||||||
|
生成的迁移文件会自动包含添加 `latitude` 和 `longitude` 字段的SQL语句。您可以检查文件内容确保迁移逻辑正确。
|
||||||
|
|
||||||
|
### 3. 执行迁移
|
||||||
|
|
||||||
|
运行以下命令执行迁移,将更改应用到数据库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run migration:run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 手动迁移SQL(可选)
|
||||||
|
|
||||||
|
如果需要手动执行SQL,可以使用以下语句:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE `area`
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 确保在执行迁移前备份数据库
|
||||||
|
- 迁移不会影响现有数据,新增字段默认为 NULL
|
||||||
|
- 迁移后,可以通过API开始为区域添加坐标信息
|
||||||
|
|
@ -46,7 +46,9 @@
|
||||||
"build": "mwtsc --cleanOutDir",
|
"build": "mwtsc --cleanOutDir",
|
||||||
"seed": "ts-node src/db/seed/index.ts",
|
"seed": "ts-node src/db/seed/index.ts",
|
||||||
"seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/db/datasource.ts",
|
"seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/db/datasource.ts",
|
||||||
"typeorm": "ts-node ./node_modules/typeorm/cli.js"
|
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
|
||||||
|
"migration:generate": "npm run typeorm -- -d src/db/datasource.ts migration:generate",
|
||||||
|
"migration:run": "npm run typeorm -- migration:run -d src/db/datasource.ts"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import { Dict } from '../entity/dict.entity';
|
||||||
import { DictItem } from '../entity/dict_item.entity';
|
import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { Template } from '../entity/template.entity';
|
import { Template } from '../entity/template.entity';
|
||||||
import { Area } from '../entity/area.entity';
|
import { Area } from '../entity/area.entity';
|
||||||
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
import DictSeeder from '../db/seeds/dict.seeder';
|
import DictSeeder from '../db/seeds/dict.seeder';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -44,6 +45,7 @@ export default {
|
||||||
default: {
|
default: {
|
||||||
entities: [
|
entities: [
|
||||||
Product,
|
Product,
|
||||||
|
ProductStockComponent,
|
||||||
WpProduct,
|
WpProduct,
|
||||||
Variation,
|
Variation,
|
||||||
User,
|
User,
|
||||||
|
|
@ -111,7 +113,7 @@ export default {
|
||||||
// wpApiUrl: 'http://localhost:10004',
|
// wpApiUrl: 'http://localhost:10004',
|
||||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||||
// siteName: 'Local',
|
// name: 'Local',
|
||||||
// email: 'tom@yoonevape.com',
|
// email: 'tom@yoonevape.com',
|
||||||
// emailPswd: '',
|
// emailPswd: '',
|
||||||
// },
|
// },
|
||||||
|
|
|
||||||
|
|
@ -11,30 +11,9 @@ import {
|
||||||
} from '@midwayjs/core';
|
} from '@midwayjs/core';
|
||||||
import { ProductService } from '../service/product.service';
|
import { ProductService } from '../service/product.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import {
|
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto';
|
||||||
BatchSetSkuDTO,
|
|
||||||
CreateBrandDTO,
|
|
||||||
CreateFlavorsDTO,
|
|
||||||
CreateProductDTO,
|
|
||||||
CreateStrengthDTO,
|
|
||||||
QueryBrandDTO,
|
|
||||||
QueryFlavorsDTO,
|
|
||||||
QueryProductDTO,
|
|
||||||
QueryStrengthDTO,
|
|
||||||
UpdateBrandDTO,
|
|
||||||
UpdateFlavorsDTO,
|
|
||||||
UpdateProductDTO,
|
|
||||||
UpdateStrengthDTO,
|
|
||||||
} from '../dto/product.dto';
|
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import {
|
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
||||||
BooleanRes,
|
|
||||||
ProductBrandListRes,
|
|
||||||
ProductBrandRes,
|
|
||||||
ProductListRes,
|
|
||||||
ProductRes,
|
|
||||||
ProductsRes,
|
|
||||||
} from '../dto/reponse.dto';
|
|
||||||
|
|
||||||
@Controller('/product')
|
@Controller('/product')
|
||||||
export class ProductController {
|
export class ProductController {
|
||||||
|
|
@ -93,9 +72,7 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ type: ProductRes })
|
||||||
type: ProductRes,
|
|
||||||
})
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async createProduct(@Body() productData: CreateProductDTO) {
|
async createProduct(@Body() productData: CreateProductDTO) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -106,14 +83,9 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ type: ProductRes })
|
||||||
type: ProductRes,
|
|
||||||
})
|
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
async updateProduct(
|
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
|
||||||
@Param('id') id: number,
|
|
||||||
@Body() productData: UpdateProductDTO
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const data = this.productService.updateProduct(id, productData);
|
const data = this.productService.updateProduct(id, productData);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -122,14 +94,9 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ type: ProductRes })
|
||||||
type: ProductRes,
|
|
||||||
})
|
|
||||||
@Put('updateNameCn/:id/:nameCn')
|
@Put('updateNameCn/:id/:nameCn')
|
||||||
async updateProductNameCn(
|
async updateProductNameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
||||||
@Param('id') id: number,
|
|
||||||
@Param('nameCn') nameCn: string
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const data = this.productService.updateProductNameCn(id, nameCn);
|
const data = this.productService.updateProductNameCn(id, nameCn);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -138,9 +105,7 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
type: BooleanRes,
|
|
||||||
})
|
|
||||||
@Del('/:id')
|
@Del('/:id')
|
||||||
async deleteProduct(@Param('id') id: number) {
|
async deleteProduct(@Param('id') id: number) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -151,14 +116,55 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
// 中文注释:获取产品的库存组成
|
||||||
type: ProductBrandListRes,
|
@ApiOkResponse()
|
||||||
})
|
@Get('/:id/components')
|
||||||
@Get('/brands')
|
async getProductComponents(@Param('id') id: number) {
|
||||||
async getBrands(@Query() query: QueryBrandDTO) {
|
|
||||||
const { current = 1, pageSize = 10, name } = query;
|
|
||||||
try {
|
try {
|
||||||
let data = await this.productService.getBrandList(
|
const data = await this.productService.getProductComponents(id);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:设置产品的库存组成(覆盖式)
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Post('/:id/components')
|
||||||
|
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.setProductComponents(id, body?.items || []);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Post('/:id/components/auto')
|
||||||
|
async autoBindComponents(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.autoBindComponentsBySku(id);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 通用属性接口:分页列表
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/attribute')
|
||||||
|
async getAttributeList(
|
||||||
|
@Query('dictName') dictName: string,
|
||||||
|
@Query('current') current = 1,
|
||||||
|
@Query('pageSize') pageSize = 10,
|
||||||
|
@Query('name') name?: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getAttributeList(
|
||||||
|
dictName,
|
||||||
{ current, pageSize },
|
{ current, pageSize },
|
||||||
name
|
name
|
||||||
);
|
);
|
||||||
|
|
@ -168,95 +174,142 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通用属性接口:全部列表
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Get('/brandAll')
|
@Get('/attributeAll')
|
||||||
async getBrandAll() {
|
async getAttributeAll(@Query('dictName') dictName: string) {
|
||||||
try {
|
try {
|
||||||
let data = await this.productService.getBrandAll();
|
const data = await this.productService.getAttributeAll(dictName);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
// 通用属性接口:创建
|
||||||
type: ProductBrandRes,
|
@ApiOkResponse()
|
||||||
})
|
@Post('/attribute')
|
||||||
@Post('/brand')
|
async createAttribute(
|
||||||
async createBrand(@Body() brandData: CreateBrandDTO) {
|
@Query('dictName') dictName: string,
|
||||||
try {
|
@Body() body: { title: string; name: string }
|
||||||
const hasBrand = await this.productService.hasAttribute(
|
|
||||||
'brand',
|
|
||||||
brandData.name
|
|
||||||
);
|
|
||||||
if (hasBrand) {
|
|
||||||
return errorResponse('品牌已存在');
|
|
||||||
}
|
|
||||||
let data = await this.productService.createBrand(brandData);
|
|
||||||
return successResponse(data);
|
|
||||||
} catch (error) {
|
|
||||||
return errorResponse(error?.message || error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOkResponse({
|
|
||||||
type: ProductBrandRes,
|
|
||||||
})
|
|
||||||
@Put('/brand/:id')
|
|
||||||
async updateBrand(
|
|
||||||
@Param('id') id: number,
|
|
||||||
@Body() brandData: UpdateBrandDTO
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const hasBrand = await this.productService.hasAttribute(
|
const hasItem = await this.productService.hasAttribute(
|
||||||
'brand',
|
dictName,
|
||||||
brandData.name,
|
body.name
|
||||||
|
);
|
||||||
|
if (hasItem) return errorResponse('字典项已存在');
|
||||||
|
const data = await this.productService.createAttribute(dictName, body);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用属性接口:更新
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Put('/attribute/:id')
|
||||||
|
async updateAttribute(
|
||||||
|
@Param('id') id: number,
|
||||||
|
@Query('dictName') dictName: string,
|
||||||
|
@Body() body: { title?: string; name?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (body?.name) {
|
||||||
|
const hasItem = await this.productService.hasAttribute(
|
||||||
|
dictName,
|
||||||
|
body.name,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
if (hasBrand) {
|
if (hasItem) return errorResponse('字典项已存在');
|
||||||
return errorResponse('品牌已存在');
|
|
||||||
}
|
}
|
||||||
const data = this.productService.updateBrand(id, brandData);
|
const data = await this.productService.updateAttribute(id, body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
// 通用属性接口:删除
|
||||||
type: BooleanRes,
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
})
|
@Del('/attribute/:id')
|
||||||
@Del('/brand/:id')
|
async deleteAttribute(@Param('id') id: number) {
|
||||||
async deleteBrand(@Param('id') id: number) {
|
|
||||||
try {
|
try {
|
||||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
await this.productService.deleteAttribute(id);
|
||||||
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
|
return successResponse(true);
|
||||||
const data = await this.productService.deleteBrand(id);
|
|
||||||
return successResponse(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/batchSetSku')
|
// 兼容旧接口:品牌
|
||||||
@ApiOkResponse({
|
@ApiOkResponse()
|
||||||
description: '批量设置 sku 的响应结果',
|
@Get('/brandAll')
|
||||||
type: BooleanRes,
|
async compatBrandAll() {
|
||||||
})
|
|
||||||
async batchSetSku(@Body() body: BatchSetSkuDTO) {
|
|
||||||
try {
|
try {
|
||||||
const result = await this.productService.batchSetSku(body.skus);
|
const data = await this.productService.getAttributeAll('brand'); // 中文注释:返回所有品牌字典项
|
||||||
return successResponse(result, '批量设置 sku 成功');
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error.message, 400);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Get('/flavorsAll')
|
@Get('/brands')
|
||||||
async getFlavorsAll() {
|
async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||||
try {
|
try {
|
||||||
let data = await this.productService.getFlavorsAll();
|
const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 中文注释:分页品牌列表
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Post('/brand')
|
||||||
|
async compatCreateBrand(@Body() body: { title: string; name: string }) {
|
||||||
|
try {
|
||||||
|
const has = await this.productService.hasAttribute('brand', body.name); // 中文注释:唯一性校验
|
||||||
|
if (has) return errorResponse('品牌已存在');
|
||||||
|
const data = await this.productService.createAttribute('brand', body); // 中文注释:创建品牌字典项
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Put('/brand/:id')
|
||||||
|
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); // 中文注释:唯一性校验(排除自身)
|
||||||
|
if (has) return errorResponse('品牌已存在');
|
||||||
|
}
|
||||||
|
const data = await this.productService.updateAttribute(id, body); // 中文注释:更新品牌字典项
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
|
@Del('/brand/:id')
|
||||||
|
async compatDeleteBrand(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
await this.productService.deleteAttribute(id); // 中文注释:删除品牌字典项
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧接口:口味
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/flavorsAll')
|
||||||
|
async compatFlavorsAll() {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getAttributeAll('flavor');
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -265,13 +318,9 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Get('/flavors')
|
@Get('/flavors')
|
||||||
async getFlavors(@Query() query: QueryFlavorsDTO) {
|
async compatFlavors(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||||
const { current = 1, pageSize = 10, name } = query;
|
|
||||||
try {
|
try {
|
||||||
let data = await this.productService.getFlavorsList(
|
const data = await this.productService.getAttributeList('flavor', { current, pageSize }, name);
|
||||||
{ current, pageSize },
|
|
||||||
name
|
|
||||||
);
|
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -280,13 +329,11 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/flavors')
|
@Post('/flavors')
|
||||||
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
|
async compatCreateFlavors(@Body() body: { title: string; name: string }) {
|
||||||
try {
|
try {
|
||||||
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name);
|
const has = await this.productService.hasAttribute('flavor', body.name);
|
||||||
if (hasFlavors) {
|
if (has) return errorResponse('口味已存在');
|
||||||
return errorResponse('口味已存在');
|
const data = await this.productService.createAttribute('flavor', body);
|
||||||
}
|
|
||||||
let data = await this.productService.createFlavors(flavorsData);
|
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -295,42 +342,36 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/flavors/:id')
|
@Put('/flavors/:id')
|
||||||
async updateFlavors(
|
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
@Param('id') id: number,
|
|
||||||
@Body() flavorsData: UpdateFlavorsDTO
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name, id);
|
if (body?.name) {
|
||||||
if (hasFlavors) {
|
const has = await this.productService.hasAttribute('flavor', body.name, id);
|
||||||
return errorResponse('口味已存在');
|
if (has) return errorResponse('口味已存在');
|
||||||
}
|
}
|
||||||
const data = this.productService.updateFlavors(id, flavorsData);
|
const data = await this.productService.updateAttribute(id, body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
type: BooleanRes,
|
|
||||||
})
|
|
||||||
@Del('/flavors/:id')
|
@Del('/flavors/:id')
|
||||||
async deleteFlavors(@Param('id') id: number) {
|
async compatDeleteFlavors(@Param('id') id: number) {
|
||||||
try {
|
try {
|
||||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
await this.productService.deleteAttribute(id);
|
||||||
if (hasProducts) throw new Error('该口味下有商品,无法删除');
|
return successResponse(true);
|
||||||
const data = await this.productService.deleteFlavors(id);
|
|
||||||
return successResponse(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兼容旧接口:规格
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Get('/strengthAll')
|
@Get('/strengthAll')
|
||||||
async getStrengthAll() {
|
async compatStrengthAll() {
|
||||||
try {
|
try {
|
||||||
let data = await this.productService.getStrengthAll();
|
const data = await this.productService.getAttributeAll('strength');
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -339,13 +380,9 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Get('/strength')
|
@Get('/strength')
|
||||||
async getStrength(@Query() query: QueryStrengthDTO) {
|
async compatStrength(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||||
const { current = 1, pageSize = 10, name } = query;
|
|
||||||
try {
|
try {
|
||||||
let data = await this.productService.getStrengthList(
|
const data = await this.productService.getAttributeList('strength', { current, pageSize }, name);
|
||||||
{ current, pageSize },
|
|
||||||
name
|
|
||||||
);
|
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -354,16 +391,11 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/strength')
|
@Post('/strength')
|
||||||
async createStrength(@Body() strengthData: CreateStrengthDTO) {
|
async compatCreateStrength(@Body() body: { title: string; name: string }) {
|
||||||
try {
|
try {
|
||||||
const hasStrength = await this.productService.hasAttribute(
|
const has = await this.productService.hasAttribute('strength', body.name);
|
||||||
'strength',
|
if (has) return errorResponse('规格已存在');
|
||||||
strengthData.name
|
const data = await this.productService.createAttribute('strength', body);
|
||||||
);
|
|
||||||
if (hasStrength) {
|
|
||||||
return errorResponse('规格已存在');
|
|
||||||
}
|
|
||||||
let data = await this.productService.createStrength(strengthData);
|
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -372,38 +404,89 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/strength/:id')
|
@Put('/strength/:id')
|
||||||
async updateStrength(
|
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
@Param('id') id: number,
|
|
||||||
@Body() strengthData: UpdateStrengthDTO
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const hasStrength = await this.productService.hasAttribute(
|
if (body?.name) {
|
||||||
'strength',
|
const has = await this.productService.hasAttribute('strength', body.name, id);
|
||||||
strengthData.name,
|
if (has) return errorResponse('规格已存在');
|
||||||
id
|
|
||||||
);
|
|
||||||
if (hasStrength) {
|
|
||||||
return errorResponse('规格已存在');
|
|
||||||
}
|
}
|
||||||
const data = this.productService.updateStrength(id, strengthData);
|
const data = await this.productService.updateAttribute(id, body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
type: BooleanRes,
|
|
||||||
})
|
|
||||||
@Del('/strength/:id')
|
@Del('/strength/:id')
|
||||||
async deleteStrength(@Param('id') id: number) {
|
async compatDeleteStrength(@Param('id') id: number) {
|
||||||
try {
|
try {
|
||||||
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
await this.productService.deleteAttribute(id);
|
||||||
if (hasProducts) throw new Error('该规格下有商品,无法删除');
|
return successResponse(true);
|
||||||
const data = await this.productService.deleteStrength(id);
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧接口:尺寸
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/sizeAll')
|
||||||
|
async compatSizeAll() {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getAttributeAll('size');
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/size')
|
||||||
|
async compatSize(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.getAttributeList('size', { current, pageSize }, name);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Post('/size')
|
||||||
|
async compatCreateSize(@Body() body: { title: string; name: string }) {
|
||||||
|
try {
|
||||||
|
const has = await this.productService.hasAttribute('size', body.name);
|
||||||
|
if (has) return errorResponse('尺寸已存在');
|
||||||
|
const data = await this.productService.createAttribute('size', body);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Put('/size/:id')
|
||||||
|
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
||||||
|
try {
|
||||||
|
if (body?.name) {
|
||||||
|
const has = await this.productService.hasAttribute('size', body.name, id);
|
||||||
|
if (has) return errorResponse('尺寸已存在');
|
||||||
|
}
|
||||||
|
const data = await this.productService.updateAttribute(id, body);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
|
@Del('/size/:id')
|
||||||
|
async compatDeleteSize(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
await this.productService.deleteAttribute(id);
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export class SiteController {
|
||||||
async all() {
|
async all() {
|
||||||
try {
|
try {
|
||||||
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
|
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
|
||||||
return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName })));
|
return successResponse(items.map((v: any) => ({ id: v.id, name: v.name })));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || '获取失败');
|
return errorResponse(error?.message || '获取失败');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,11 @@ export class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/add')
|
@Post('/add')
|
||||||
async addUser(@Body() body: { username: string; password: string }) {
|
async addUser(@Body() body: { username: string; password: string; remark?: string }) {
|
||||||
const { username, password } = body;
|
const { username, password, remark } = body;
|
||||||
try {
|
try {
|
||||||
await this.userService.addUser(username, password);
|
// 中文注释:新增用户(支持备注)
|
||||||
|
await this.userService.addUser(username, password, remark);
|
||||||
return successResponse(true);
|
return successResponse(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
@ -52,21 +53,76 @@ export class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
async listUsers(@Query() query: { current: number; pageSize: number }) {
|
async listUsers(
|
||||||
const { current = 1, pageSize = 10 } = query;
|
@Query()
|
||||||
return successResponse(await this.userService.listUsers(current, pageSize));
|
query: {
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
remark?: string;
|
||||||
|
username?: string;
|
||||||
|
isActive?: string;
|
||||||
|
isSuper?: string;
|
||||||
|
isAdmin?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
isActive: toBool(isActive),
|
||||||
|
isSuper: toBool(isSuper),
|
||||||
|
isAdmin: toBool(isAdmin),
|
||||||
|
});
|
||||||
|
const safeItems = (items || []).map((it: any) => {
|
||||||
|
const { password, ...rest } = it || {};
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
return successResponse({ items: safeItems, total, current, pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/toggleActive')
|
@Post('/toggleActive')
|
||||||
async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
|
async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
|
||||||
return this.userService.toggleUserActive(body.userId, body.isActive);
|
try {
|
||||||
|
// 中文注释:调用服务层更新启用状态
|
||||||
|
const data = await this.userService.toggleUserActive(body.userId, body.isActive);
|
||||||
|
// 中文注释:移除密码字段,保证安全
|
||||||
|
const { password, ...safe } = data as any;
|
||||||
|
return successResponse(safe);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || '操作失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:更新用户(支持用户名/密码/权限/角色更新)
|
||||||
|
@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)
|
||||||
|
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) {
|
||||||
|
return errorResponse(error?.message || '更新失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Get()
|
@Get()
|
||||||
async getUser(@User() user) {
|
async getUser(@User() user) {
|
||||||
try {
|
try {
|
||||||
return successResponse(await this.userService.getUser(user.id));
|
// 中文注释:详情移除密码字段
|
||||||
|
const data = await this.userService.getUser(user.id);
|
||||||
|
const { password, ...safe } = (data as any) || {};
|
||||||
|
return successResponse(safe);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse('获取失败');
|
return errorResponse('获取失败');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,6 @@
|
||||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
import { SeederOptions } from 'typeorm-extension';
|
import { SeederOptions } from 'typeorm-extension';
|
||||||
import { Product } from '../entity/product.entity';
|
|
||||||
import { WpProduct } from '../entity/wp_product.entity';
|
|
||||||
import { Variation } from '../entity/variation.entity';
|
|
||||||
import { User } from '../entity/user.entity';
|
|
||||||
import { PurchaseOrder } from '../entity/purchase_order.entity';
|
|
||||||
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
|
|
||||||
import { Stock } from '../entity/stock.entity';
|
|
||||||
import { StockPoint } from '../entity/stock_point.entity';
|
|
||||||
import { StockRecord } from '../entity/stock_record.entity';
|
|
||||||
import { Order } from '../entity/order.entity';
|
|
||||||
import { OrderItem } from '../entity/order_item.entity';
|
|
||||||
import { OrderCoupon } from '../entity/order_coupon.entity';
|
|
||||||
import { OrderFee } from '../entity/order_fee.entity';
|
|
||||||
import { OrderRefund } from '../entity/order_refund.entity';
|
|
||||||
import { OrderRefundItem } from '../entity/order_refund_item.entity';
|
|
||||||
import { OrderSale } from '../entity/order_sale.entity';
|
|
||||||
import { OrderSaleOriginal } from '../entity/order_item_original.entity';
|
|
||||||
import { OrderShipping } from '../entity/order_shipping.entity';
|
|
||||||
import { Service } from '../entity/service.entity';
|
|
||||||
import { ShippingAddress } from '../entity/shipping_address.entity';
|
|
||||||
import { OrderNote } from '../entity/order_note.entity';
|
|
||||||
import { OrderShipment } from '../entity/order_shipment.entity';
|
|
||||||
import { Shipment } from '../entity/shipment.entity';
|
|
||||||
import { ShipmentItem } from '../entity/shipment_item.entity';
|
|
||||||
import { Transfer } from '../entity/transfer.entity';
|
|
||||||
import { TransferItem } from '../entity/transfer_item.entity';
|
|
||||||
import { CustomerTag } from '../entity/customer_tag.entity';
|
|
||||||
import { Customer } from '../entity/customer.entity';
|
|
||||||
import { DeviceWhitelist } from '../entity/device_whitelist';
|
|
||||||
import { AuthCode } from '../entity/auth_code';
|
|
||||||
import { Subscription } from '../entity/subscription.entity';
|
|
||||||
import { Site } from '../entity/site.entity';
|
|
||||||
import { Dict } from '../entity/dict.entity';
|
|
||||||
import { DictItem } from '../entity/dict_item.entity';
|
|
||||||
import { Template } from '../entity/template.entity';
|
|
||||||
import { Area } from '../entity/area.entity';
|
|
||||||
|
|
||||||
const options: DataSourceOptions & SeederOptions = {
|
const options: DataSourceOptions & SeederOptions = {
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
|
|
@ -46,44 +11,7 @@ const options: DataSourceOptions & SeederOptions = {
|
||||||
database: 'inventory',
|
database: 'inventory',
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: true,
|
logging: true,
|
||||||
entities: [
|
entities: [__dirname + '/../entity/*.ts'],
|
||||||
Product,
|
|
||||||
WpProduct,
|
|
||||||
Variation,
|
|
||||||
User,
|
|
||||||
PurchaseOrder,
|
|
||||||
PurchaseOrderItem,
|
|
||||||
Stock,
|
|
||||||
StockPoint,
|
|
||||||
StockRecord,
|
|
||||||
Order,
|
|
||||||
OrderItem,
|
|
||||||
OrderCoupon,
|
|
||||||
OrderFee,
|
|
||||||
OrderRefund,
|
|
||||||
OrderRefundItem,
|
|
||||||
OrderSale,
|
|
||||||
OrderSaleOriginal,
|
|
||||||
OrderShipment,
|
|
||||||
ShipmentItem,
|
|
||||||
Shipment,
|
|
||||||
OrderShipping,
|
|
||||||
Service,
|
|
||||||
ShippingAddress,
|
|
||||||
OrderNote,
|
|
||||||
Transfer,
|
|
||||||
TransferItem,
|
|
||||||
CustomerTag,
|
|
||||||
Customer,
|
|
||||||
DeviceWhitelist,
|
|
||||||
AuthCode,
|
|
||||||
Subscription,
|
|
||||||
Site,
|
|
||||||
Dict,
|
|
||||||
DictItem,
|
|
||||||
Template,
|
|
||||||
Area,
|
|
||||||
],
|
|
||||||
migrations: ['src/db/migrations/**/*.ts'],
|
migrations: ['src/db/migrations/**/*.ts'],
|
||||||
seeds: ['src/db/seeds/**/*.ts'],
|
seeds: ['src/db/seeds/**/*.ts'],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export class Area1764294088896 implements MigrationInterface {
|
||||||
// await queryRunner.query(`CREATE TABLE \`area\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_644ffaf8fbde4db798cb47712f\` (\`name\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
// await queryRunner.query(`CREATE TABLE \`area\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_644ffaf8fbde4db798cb47712f\` (\`name\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||||
// await queryRunner.query(`CREATE TABLE \`stock_point_areas_area\` (\`stockPointId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_07d2db2150151e2ef341d2f1de\` (\`stockPointId\`), INDEX \`IDX_92707ea81fc19dc707dba24819\` (\`areaId\`), PRIMARY KEY (\`stockPointId\`, \`areaId\`)) ENGINE=InnoDB`);
|
// await queryRunner.query(`CREATE TABLE \`stock_point_areas_area\` (\`stockPointId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_07d2db2150151e2ef341d2f1de\` (\`stockPointId\`), INDEX \`IDX_92707ea81fc19dc707dba24819\` (\`areaId\`), PRIMARY KEY (\`stockPointId\`, \`areaId\`)) ENGINE=InnoDB`);
|
||||||
// await queryRunner.query(`CREATE TABLE \`site_areas_area\` (\`siteId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_926a14ac4c91f38792831acd2a\` (\`siteId\`), INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` (\`areaId\`), PRIMARY KEY (\`siteId\`, \`areaId\`)) ENGINE=InnoDB`);
|
// await queryRunner.query(`CREATE TABLE \`site_areas_area\` (\`siteId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_926a14ac4c91f38792831acd2a\` (\`siteId\`), INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` (\`areaId\`), PRIMARY KEY (\`siteId\`, \`areaId\`)) ENGINE=InnoDB`);
|
||||||
// await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`siteName\``);
|
// await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`name\``);
|
||||||
// await queryRunner.query(`ALTER TABLE `product` ADD `promotionPrice` decimal(10,2) NOT NULL DEFAULT '0.00'`);
|
// await queryRunner.query(`ALTER TABLE `product` ADD `promotionPrice` decimal(10,2) NOT NULL DEFAULT '0.00'`);
|
||||||
// await queryRunner.query(`ALTER TABLE `product` ADD `source` int NOT NULL DEFAULT '0'`);
|
// await queryRunner.query(`ALTER TABLE `product` ADD `source` int NOT NULL DEFAULT '0'`);
|
||||||
// await queryRunner.query(`ALTER TABLE \`site\` ADD \`token\` varchar(255) NULL`);
|
// await queryRunner.query(`ALTER TABLE \`site\` ADD \`token\` varchar(255) NULL`);
|
||||||
|
|
@ -30,7 +30,7 @@ export class Area1764294088896 implements MigrationInterface {
|
||||||
await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`token\``);
|
await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`token\``);
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`source\``);
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`source\``);
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`promotionPrice\``);
|
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`promotionPrice\``);
|
||||||
await queryRunner.query(`ALTER TABLE \`site\` ADD \`siteName\` varchar(255) NOT NULL`);
|
await queryRunner.query(`ALTER TABLE \`site\` ADD \`name\` varchar(255) NOT NULL`);
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` ON \`site_areas_area\``);
|
await queryRunner.query(`DROP INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` ON \`site_areas_area\``);
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_926a14ac4c91f38792831acd2a\` ON \`site_areas_area\``);
|
await queryRunner.query(`DROP INDEX \`IDX_926a14ac4c91f38792831acd2a\` ON \`site_areas_area\``);
|
||||||
await queryRunner.query(`DROP TABLE \`site_areas_area\``);
|
await queryRunner.query(`DROP TABLE \`site_areas_area\``);
|
||||||
|
|
@ -39,7 +39,7 @@ export class Area1764294088896 implements MigrationInterface {
|
||||||
await queryRunner.query(`DROP TABLE \`stock_point_areas_area\``);
|
await queryRunner.query(`DROP TABLE \`stock_point_areas_area\``);
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_644ffaf8fbde4db798cb47712f\` ON \`area\``);
|
await queryRunner.query(`DROP INDEX \`IDX_644ffaf8fbde4db798cb47712f\` ON \`area\``);
|
||||||
await queryRunner.query(`DROP TABLE \`area\``);
|
await queryRunner.query(`DROP TABLE \`area\``);
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\` (\`siteName\`)`);
|
await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\` (\`name\`)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class ProductStock1764299629279 implements MigrationInterface {
|
||||||
|
name = 'ProductStock1764299629279'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE \`order_item_original\` (\`id\` int NOT NULL AUTO_INCREMENT, \`order_id\` int NOT NULL, \`name\` varchar(255) NOT NULL, \`siteId\` varchar(255) NOT NULL, \`externalOrderId\` varchar(255) NOT NULL, \`externalOrderItemId\` varchar(255) NULL, \`externalProductId\` varchar(255) NOT NULL, \`externalVariationId\` varchar(255) NOT NULL, \`quantity\` int NOT NULL, \`subtotal\` decimal(10,2) NULL, \`subtotal_tax\` decimal(10,2) NULL, \`total\` decimal(10,2) NULL, \`total_tax\` decimal(10,2) NULL, \`sku\` varchar(255) NULL, \`price\` decimal(10,2) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD CONSTRAINT \`FK_ca48e4bce0bb8cecd24cc8081e5\` FOREIGN KEY (\`order_id\`) REFERENCES \`order\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP FOREIGN KEY \`FK_ca48e4bce0bb8cecd24cc8081e5\``);
|
||||||
|
await queryRunner.query(`DROP TABLE \`order_item_original\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,14 @@ export class CreateAreaDTO {
|
||||||
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
|
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
|
||||||
@Rule(RuleType.string().required())
|
@Rule(RuleType.string().required())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', description: '纬度', example: 48.8566, required: false })
|
||||||
|
@Rule(RuleType.number().min(-90).max(90).allow(null))
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', description: '经度', example: 2.3522, required: false })
|
||||||
|
@Rule(RuleType.number().min(-180).max(180).allow(null))
|
||||||
|
longitude?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新区域的数据传输对象
|
// 更新区域的数据传输对象
|
||||||
|
|
@ -14,6 +22,14 @@ export class UpdateAreaDTO {
|
||||||
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
|
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', description: '纬度', example: 48.8566, required: false })
|
||||||
|
@Rule(RuleType.number().min(-90).max(90).allow(null))
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', description: '经度', example: 2.3522, required: false })
|
||||||
|
@Rule(RuleType.number().min(-180).max(180).allow(null))
|
||||||
|
longitude?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询区域的数据传输对象
|
// 查询区域的数据传输对象
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ export class CreateDictItemDTO {
|
||||||
@Rule(RuleType.string().required())
|
@Rule(RuleType.string().required())
|
||||||
title: string; // 字典项标题
|
title: string; // 字典项标题
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow(null))
|
||||||
|
titleCN?: string; // 字典项中文标题 (可选)
|
||||||
|
|
||||||
@Rule(RuleType.number().required())
|
@Rule(RuleType.number().required())
|
||||||
dictId: number; // 所属字典的ID
|
dictId: number; // 所属字典的ID
|
||||||
}
|
}
|
||||||
|
|
@ -37,4 +40,11 @@ export class UpdateDictItemDTO {
|
||||||
|
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
title?: string; // 字典项标题 (可选)
|
title?: string; // 字典项标题 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow(null))
|
||||||
|
titleCN?: string; // 字典项中文标题 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow(null))
|
||||||
|
value?: string; // 字典项值 (可选)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,6 @@
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { Rule, RuleType } from '@midwayjs/validate';
|
import { Rule, RuleType } from '@midwayjs/validate';
|
||||||
|
|
||||||
class DictItemDTO {
|
|
||||||
@ApiProperty({ description: '显示名称', required: false })
|
|
||||||
@Rule(RuleType.string())
|
|
||||||
title?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '唯一标识', required: true })
|
|
||||||
@Rule(RuleType.string().required())
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO 用于创建产品
|
* DTO 用于创建产品
|
||||||
*/
|
*/
|
||||||
|
|
@ -31,50 +21,52 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '品牌', type: DictItemDTO })
|
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||||
@Rule(
|
@ApiProperty({ description: '属性列表', type: 'array' })
|
||||||
RuleType.object().keys({
|
@Rule(RuleType.array().required())
|
||||||
title: RuleType.string().required(),
|
attributes: AttributeInputDTO[];
|
||||||
name: RuleType.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
brand: DictItemDTO;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '规格', type: DictItemDTO })
|
|
||||||
@Rule(
|
|
||||||
RuleType.object().keys({
|
|
||||||
title: RuleType.string().required(),
|
|
||||||
name: RuleType.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
strength: DictItemDTO;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '口味', type: DictItemDTO })
|
|
||||||
@Rule(
|
|
||||||
RuleType.object().keys({
|
|
||||||
title: RuleType.string().required(),
|
|
||||||
name: RuleType.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
flavor: DictItemDTO;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@Rule(RuleType.string())
|
|
||||||
humidity: string;
|
|
||||||
|
|
||||||
// 商品价格
|
// 商品价格
|
||||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
price?: number;
|
price?: number;
|
||||||
|
|
||||||
|
// 促销价格
|
||||||
|
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
promotionPrice?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO 用于更新产品
|
* DTO 用于更新产品
|
||||||
*/
|
*/
|
||||||
export class UpdateProductDTO extends CreateProductDTO {
|
export class UpdateProductDTO {
|
||||||
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
|
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
name: string;
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
sku?: string;
|
||||||
|
|
||||||
|
// 商品价格
|
||||||
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
price?: number;
|
||||||
|
|
||||||
|
// 促销价格
|
||||||
|
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
promotionPrice?: number;
|
||||||
|
|
||||||
|
// 属性更新(中文注释:可选,支持增量替换指定字典的属性项)
|
||||||
|
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||||
|
@Rule(RuleType.array())
|
||||||
|
attributes?: AttributeInputDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,6 +90,25 @@ export class QueryProductDTO {
|
||||||
brandId: number;
|
brandId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息)
|
||||||
|
export class AttributeInputDTO {
|
||||||
|
@ApiProperty({ description: '字典名称', example: 'brand', required: false})
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
dictName?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '字典项 ID', required: false })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '字典项显示名称', required: false })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '字典项唯一标识', required: false })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO 用于创建品牌
|
* DTO 用于创建品牌
|
||||||
*/
|
*/
|
||||||
|
|
@ -213,6 +224,41 @@ export class QueryStrengthDTO {
|
||||||
name: string; // 搜索关键字(支持模糊查询)
|
name: string; // 搜索关键字(支持模糊查询)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// size 新增 DTO
|
||||||
|
export class CreateSizeDTO {
|
||||||
|
@ApiProperty({ example: '6', description: '尺寸名称', required: false })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '6', description: '尺寸唯一标识', required: true })
|
||||||
|
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateSizeDTO {
|
||||||
|
@ApiProperty({ example: '6', description: '尺寸名称' })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '6', description: '尺寸唯一标识' })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QuerySizeDTO {
|
||||||
|
@ApiProperty({ example: '1', description: '页码' })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
current: number; // 页码
|
||||||
|
|
||||||
|
@ApiProperty({ example: '10', description: '每页大小' })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
pageSize: number; // 每页大小
|
||||||
|
|
||||||
|
@ApiProperty({ example: '6', description: '关键字' })
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
name: string; // 搜索关键字(支持模糊查询)
|
||||||
|
}
|
||||||
|
|
||||||
export class SkuItemDTO {
|
export class SkuItemDTO {
|
||||||
@ApiProperty({ description: '产品 ID' })
|
@ApiProperty({ description: '产品 ID' })
|
||||||
productId: number;
|
productId: number;
|
||||||
|
|
@ -225,3 +271,21 @@ export class BatchSetSkuDTO {
|
||||||
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
|
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
|
||||||
skus: SkuItemDTO[];
|
skus: SkuItemDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文注释:产品库存组成项输入
|
||||||
|
export class ProductComponentItemDTO {
|
||||||
|
@ApiProperty({ description: '库存记录ID' })
|
||||||
|
@Rule(RuleType.number().required())
|
||||||
|
stockId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '组成数量', example: 1 })
|
||||||
|
@Rule(RuleType.number().min(1).default(1))
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:设置产品库存组成输入
|
||||||
|
export class SetProductComponentsDTO {
|
||||||
|
@ApiProperty({ description: '组成项列表', type: [ProductComponentItemDTO] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.object()))
|
||||||
|
items: ProductComponentItemDTO[];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,20 @@ export class ProductStrengthListRes extends SuccessWrapper(
|
||||||
) {}
|
) {}
|
||||||
//产品规格返所有数据
|
//产品规格返所有数据
|
||||||
export class ProductStrengthAllRes extends SuccessArrayWrapper(Dict) {}
|
export class ProductStrengthAllRes extends SuccessArrayWrapper(Dict) {}
|
||||||
//产品规格返回数据
|
//产品规格返返回数据
|
||||||
export class ProductStrengthRes extends SuccessWrapper(Dict) {}
|
export class ProductStrengthRes extends SuccessWrapper(Dict) {}
|
||||||
|
|
||||||
|
// 产品尺寸返分页数据
|
||||||
|
export class SizePaginatedResponse extends PaginatedWrapper(Dict) {}
|
||||||
|
// 产品尺寸返分页返回数据
|
||||||
|
export class ProductSizeListRes extends SuccessWrapper(
|
||||||
|
SizePaginatedResponse
|
||||||
|
) {}
|
||||||
|
// 产品尺寸返所有数据
|
||||||
|
export class ProductSizeAllRes extends SuccessArrayWrapper(Dict) {}
|
||||||
|
// 产品尺寸返回数据
|
||||||
|
export class ProductSizeRes extends SuccessWrapper(Dict) {}
|
||||||
|
|
||||||
//产品分页数据
|
//产品分页数据
|
||||||
export class WpProductPaginatedResponse extends PaginatedWrapper(
|
export class WpProductPaginatedResponse extends PaginatedWrapper(
|
||||||
WpProductDTO
|
WpProductDTO
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export class SiteConfig {
|
||||||
|
|
||||||
@ApiProperty({ description: '站点名' })
|
@ApiProperty({ description: '站点名' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
siteName: string;
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
|
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
|
||||||
|
|
@ -39,7 +39,7 @@ export class CreateSiteDTO {
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
consumerSecret?: string;
|
consumerSecret?: string;
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
siteName: string;
|
name: string;
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||||
type?: string;
|
type?: string;
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
|
|
@ -54,7 +54,7 @@ export class UpdateSiteDTO {
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
consumerSecret?: string;
|
consumerSecret?: string;
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
siteName?: string;
|
name?: string;
|
||||||
@Rule(RuleType.boolean().optional())
|
@Rule(RuleType.boolean().optional())
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@ export class Area {
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', description: '纬度', required: false })
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 6, nullable: true })
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'number', description: '经度', required: false })
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 6, nullable: true })
|
||||||
|
longitude?: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2022-12-12 11:11:11',
|
example: '2022-12-12 11:11:11',
|
||||||
description: '创建时间',
|
description: '创建时间',
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ export class DictItem {
|
||||||
// 字典项名称
|
// 字典项名称
|
||||||
@Column({ comment: '字典项显示名称' })
|
@Column({ comment: '字典项显示名称' })
|
||||||
title: string;
|
title: string;
|
||||||
|
// 目前没有单独做国际化, 所以这里先添加 titleCN 用来标注
|
||||||
|
@Column({ comment: '字典项中文名称', nullable: true })
|
||||||
|
titleCN: string;
|
||||||
// 唯一标识
|
// 唯一标识
|
||||||
@Column({ unique: true, comment: '字典唯一标识名称' })
|
@Column({ unique: true, comment: '字典唯一标识名称' })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import {
|
||||||
Entity,
|
Entity,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
|
OneToMany,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { DictItem } from './dict_item.entity';
|
import { DictItem } from './dict_item.entity';
|
||||||
|
import { ProductStockComponent } from './product_stock_component.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Product {
|
export class Product {
|
||||||
|
|
@ -34,6 +36,7 @@ export class Product {
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
nameCn: string;
|
nameCn: string;
|
||||||
|
|
||||||
|
|
||||||
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
@ApiProperty({ example: '产品描述', description: '产品描述' })
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
@ -46,18 +49,30 @@ export class Product {
|
||||||
@ApiProperty({ description: '价格', example: 99.99 })
|
@ApiProperty({ description: '价格', example: 99.99 })
|
||||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
price: number;
|
price: number;
|
||||||
|
// 类型 主要用来区分混装和单品 单品死
|
||||||
|
@ApiProperty({ description: '类型' })
|
||||||
|
@Column()
|
||||||
|
type: string;
|
||||||
// 促销价格
|
// 促销价格
|
||||||
@ApiProperty({ description: '促销价格', example: 99.99 })
|
@ApiProperty({ description: '促销价格', example: 99.99 })
|
||||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
promotionPrice: number;
|
promotionPrice: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存', example: 100 })
|
||||||
|
@Column({ default: 0 })
|
||||||
|
stock: number;
|
||||||
|
|
||||||
@ManyToMany(() => DictItem, {
|
@ManyToMany(() => DictItem, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
})
|
})
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
attributes: DictItem[];
|
attributes: DictItem[];
|
||||||
|
|
||||||
|
// 中文注释:产品的库存组成,一对多关系(使用独立表)
|
||||||
|
@ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
|
||||||
|
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
||||||
|
components: ProductStockComponent[];
|
||||||
|
|
||||||
// 来源
|
// 来源
|
||||||
@ApiProperty({ description: '来源', example: '1' })
|
@ApiProperty({ description: '来源', example: '1' })
|
||||||
@Column({ default: 0 })
|
@Column({ default: 0 })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
import { Product } from './product.entity';
|
||||||
|
import { Stock } from './stock.entity';
|
||||||
|
|
||||||
|
@Entity('product_stock_component')
|
||||||
|
export class ProductStockComponent {
|
||||||
|
@ApiProperty({ type: Number })
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: Number })
|
||||||
|
@Column()
|
||||||
|
productId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: Number })
|
||||||
|
@Column()
|
||||||
|
stockId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ type: Number, description: '组成数量' })
|
||||||
|
@Column({ type: 'int', default: 1 })
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
// 中文注释:多对一,组件隶属于一个产品
|
||||||
|
@ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
|
||||||
|
product: Product;
|
||||||
|
|
||||||
|
// 中文注释:多对一,组件引用一个库存记录
|
||||||
|
@ManyToOne(() => Stock, { eager: true, onDelete: 'CASCADE' })
|
||||||
|
stock: Stock;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -28,4 +28,8 @@ export class User {
|
||||||
|
|
||||||
@Column({ default: true })
|
@Column({ default: true })
|
||||||
isActive: boolean; // 用户是否启用
|
isActive: boolean; // 用户是否启用
|
||||||
|
|
||||||
|
// 中文注释:备注字段(可选)
|
||||||
|
@Column({ nullable: true })
|
||||||
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface WpSite {
|
||||||
wpApiUrl: string;
|
wpApiUrl: string;
|
||||||
consumerKey: string;
|
consumerKey: string;
|
||||||
consumerSecret: string;
|
consumerSecret: string;
|
||||||
siteName: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
emailPswd: string;
|
emailPswd: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ export class AreaService {
|
||||||
}
|
}
|
||||||
const area = new Area();
|
const area = new Area();
|
||||||
area.name = params.name;
|
area.name = params.name;
|
||||||
|
if (params.latitude !== undefined) {
|
||||||
|
area.latitude = params.latitude;
|
||||||
|
}
|
||||||
|
if (params.longitude !== undefined) {
|
||||||
|
area.longitude = params.longitude;
|
||||||
|
}
|
||||||
return await this.areaModel.save(area);
|
return await this.areaModel.save(area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +49,12 @@ export class AreaService {
|
||||||
}
|
}
|
||||||
area.name = params.name;
|
area.name = params.name;
|
||||||
}
|
}
|
||||||
|
if (params.latitude !== undefined) {
|
||||||
|
area.latitude = params.latitude;
|
||||||
|
}
|
||||||
|
if (params.longitude !== undefined) {
|
||||||
|
area.longitude = params.longitude;
|
||||||
|
}
|
||||||
return await this.areaModel.save(area);
|
return await this.areaModel.save(area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -561,11 +561,11 @@ export class LogisticsService {
|
||||||
// 从数据库批量获取站点信息,构建映射以避免 N+1 查询
|
// 从数据库批量获取站点信息,构建映射以避免 N+1 查询
|
||||||
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
|
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
|
||||||
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
|
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
|
||||||
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName]));
|
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name]));
|
||||||
|
|
||||||
return orders.map(order => ({
|
return orders.map(order => ({
|
||||||
...order,
|
...order,
|
||||||
siteName: siteMap.get(order.siteId) || '',
|
name: siteMap.get(order.siteId) || '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1350,7 +1350,7 @@ export class OrderService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
siteName: site?.name,
|
name: site?.name,
|
||||||
// Site 实体无邮箱字段,这里返回空字符串保持兼容
|
// Site 实体无邮箱字段,这里返回空字符串保持兼容
|
||||||
email: '',
|
email: '',
|
||||||
items,
|
items,
|
||||||
|
|
@ -1418,11 +1418,11 @@ export class OrderService {
|
||||||
// 批量获取订单涉及的站点名称,避免使用配置文件
|
// 批量获取订单涉及的站点名称,避免使用配置文件
|
||||||
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
|
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
|
||||||
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
|
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
|
||||||
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName]));
|
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name]));
|
||||||
return orders.map(order => ({
|
return orders.map(order => ({
|
||||||
externalOrderId: order.externalOrderId,
|
externalOrderId: order.externalOrderId,
|
||||||
id: order.id,
|
id: order.id,
|
||||||
siteName: siteMap.get(order.siteId) || '',
|
name: siteMap.get(order.siteId) || '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1430,7 +1430,7 @@ export class OrderService {
|
||||||
const order = await this.orderModel.findOne({ where: { id } });
|
const order = await this.orderModel.findOne({ where: { id } });
|
||||||
if (!order) throw new Error(`订单 ${id}不存在`);
|
if (!order) throw new Error(`订单 ${id}不存在`);
|
||||||
const s: any = await this.siteService.get(Number(order.siteId), true);
|
const s: any = await this.siteService.get(Number(order.siteId), true);
|
||||||
const site = { id: String(s.id), wpApiUrl: s.apiUrl, consumerKey: s.consumerKey, consumerSecret: s.consumerSecret, siteName: s.siteName, email: '', emailPswd: '' } as WpSite;
|
const site = { id: String(s.id), wpApiUrl: s.apiUrl, consumerKey: s.consumerKey, consumerSecret: s.consumerSecret, name: s.name, email: '', emailPswd: '' } as WpSite;
|
||||||
if (order.status !== OrderStatus.CANCEL) {
|
if (order.status !== OrderStatus.CANCEL) {
|
||||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||||
status: OrderStatus.CANCEL,
|
status: OrderStatus.CANCEL,
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,19 @@ import {
|
||||||
CreateFlavorsDTO,
|
CreateFlavorsDTO,
|
||||||
CreateProductDTO,
|
CreateProductDTO,
|
||||||
CreateStrengthDTO,
|
CreateStrengthDTO,
|
||||||
|
CreateSizeDTO,
|
||||||
UpdateBrandDTO,
|
UpdateBrandDTO,
|
||||||
UpdateFlavorsDTO,
|
UpdateFlavorsDTO,
|
||||||
UpdateProductDTO,
|
UpdateProductDTO,
|
||||||
UpdateStrengthDTO,
|
UpdateStrengthDTO,
|
||||||
|
UpdateSizeDTO,
|
||||||
} from '../dto/product.dto';
|
} from '../dto/product.dto';
|
||||||
import {
|
import {
|
||||||
BrandPaginatedResponse,
|
BrandPaginatedResponse,
|
||||||
FlavorsPaginatedResponse,
|
FlavorsPaginatedResponse,
|
||||||
ProductPaginatedResponse,
|
ProductPaginatedResponse,
|
||||||
StrengthPaginatedResponse,
|
StrengthPaginatedResponse,
|
||||||
|
SizePaginatedResponse,
|
||||||
} from '../dto/reponse.dto';
|
} from '../dto/reponse.dto';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { WpProduct } from '../entity/wp_product.entity';
|
import { WpProduct } from '../entity/wp_product.entity';
|
||||||
|
|
@ -26,6 +29,9 @@ import { Dict } from '../entity/dict.entity';
|
||||||
import { DictItem } from '../entity/dict_item.entity';
|
import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { Context } from '@midwayjs/koa';
|
||||||
import { TemplateService } from './template.service';
|
import { TemplateService } from './template.service';
|
||||||
|
import { StockService } from './stock.service';
|
||||||
|
import { Stock } from '../entity/stock.entity';
|
||||||
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class ProductService {
|
export class ProductService {
|
||||||
|
|
@ -35,6 +41,9 @@ export class ProductService {
|
||||||
@Inject()
|
@Inject()
|
||||||
templateService: TemplateService;
|
templateService: TemplateService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
stockService: StockService;
|
||||||
|
|
||||||
@InjectEntityModel(Product)
|
@InjectEntityModel(Product)
|
||||||
productModel: Repository<Product>;
|
productModel: Repository<Product>;
|
||||||
|
|
||||||
|
|
@ -50,6 +59,12 @@ export class ProductService {
|
||||||
@InjectEntityModel(Variation)
|
@InjectEntityModel(Variation)
|
||||||
variationModel: Repository<Variation>;
|
variationModel: Repository<Variation>;
|
||||||
|
|
||||||
|
@InjectEntityModel(Stock)
|
||||||
|
stockModel: Repository<Stock>;
|
||||||
|
|
||||||
|
@InjectEntityModel(ProductStockComponent)
|
||||||
|
productStockComponentModel: Repository<ProductStockComponent>;
|
||||||
|
|
||||||
// async findProductsByName(name: string): Promise<Product[]> {
|
// async findProductsByName(name: string): Promise<Product[]> {
|
||||||
// const where: any = {};
|
// const where: any = {};
|
||||||
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
|
|
@ -157,34 +172,14 @@ export class ProductService {
|
||||||
|
|
||||||
const [items, total] = await qb.getManyAndCount();
|
const [items, total] = await qb.getManyAndCount();
|
||||||
|
|
||||||
// 格式化返回的数据
|
|
||||||
const formattedItems = items.map(product => {
|
|
||||||
const getAttributeTitle = (dictName: string) =>
|
|
||||||
product.attributes.find(a => a.dict.name === dictName)?.title || null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: product.id,
|
items,
|
||||||
name: product.name,
|
|
||||||
nameCn: product.nameCn,
|
|
||||||
description: product.description,
|
|
||||||
humidity: getAttributeTitle('humidity'),
|
|
||||||
sku: product.sku,
|
|
||||||
createdAt: product.createdAt,
|
|
||||||
updatedAt: product.updatedAt,
|
|
||||||
brandName: getAttributeTitle('brand'),
|
|
||||||
flavorsName: getAttributeTitle('flavor'),
|
|
||||||
strengthName: getAttributeTitle('strength'),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: formattedItems,
|
|
||||||
total,
|
total,
|
||||||
...pagination,
|
...pagination,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateDictItem(
|
async getOrCreateAttribute(
|
||||||
dictName: string,
|
dictName: string,
|
||||||
itemTitle: string,
|
itemTitle: string,
|
||||||
itemName?: string
|
itemName?: string
|
||||||
|
|
@ -213,31 +208,34 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||||||
const { name, description, brand, flavor, strength, humidity, sku } =
|
const { name, description, attributes, sku, price } = createProductDTO;
|
||||||
createProductDTO;
|
|
||||||
|
|
||||||
// 获取或创建品牌、口味、规格
|
// 条件判断(中文注释:校验属性输入)
|
||||||
const brandItem = await this.getOrCreateDictItem(
|
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||||||
'brand',
|
throw new Error('属性列表不能为空');
|
||||||
brand.title,
|
}
|
||||||
brand.name
|
|
||||||
);
|
|
||||||
const flavorItem = await this.getOrCreateDictItem(
|
|
||||||
'flavor',
|
|
||||||
flavor.title,
|
|
||||||
flavor.name
|
|
||||||
);
|
|
||||||
const strengthItem = await this.getOrCreateDictItem(
|
|
||||||
'strength',
|
|
||||||
strength.title,
|
|
||||||
strength.name
|
|
||||||
);
|
|
||||||
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
|
|
||||||
|
|
||||||
// 检查具有完全相同属性组合的产品是否已存在
|
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
|
||||||
const attributesToMatch = [brandItem, flavorItem, strengthItem, humidityItem];
|
const resolvedAttributes: DictItem[] = [];
|
||||||
|
for (const attr of attributes) {
|
||||||
|
let item: DictItem | null = null;
|
||||||
|
if (attr.id) {
|
||||||
|
// 中文注释:如果传入了 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 信息创建或获取字典项
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
resolvedAttributes.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查完全相同属性组合是否已存在(中文注释:避免重复)
|
||||||
const qb = this.productModel.createQueryBuilder('product');
|
const qb = this.productModel.createQueryBuilder('product');
|
||||||
attributesToMatch.forEach((attr, index) => {
|
resolvedAttributes.forEach((attr, index) => {
|
||||||
qb.innerJoin(
|
qb.innerJoin(
|
||||||
'product.attributes',
|
'product.attributes',
|
||||||
`attr${index}`,
|
`attr${index}`,
|
||||||
|
|
@ -245,30 +243,40 @@ export class ProductService {
|
||||||
{ [`attrId${index}`]: attr.id }
|
{ [`attrId${index}`]: attr.id }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const isExit = await qb.getOne();
|
const isExist = await qb.getOne();
|
||||||
|
if (isExist) throw new Error('产品已存在');
|
||||||
|
|
||||||
if (isExit) throw new Error('产品已存在');
|
// 创建新产品实例(中文注释:绑定属性与基础字段)
|
||||||
|
|
||||||
// 创建新产品实例
|
|
||||||
const product = new Product();
|
const product = new Product();
|
||||||
product.name = name;
|
product.name = name;
|
||||||
product.description = description;
|
product.description = description;
|
||||||
product.attributes = attributesToMatch;
|
product.attributes = resolvedAttributes;
|
||||||
|
|
||||||
// 如果用户提供了 sku,则直接使用;否则,通过模板引擎生成
|
// 生成或设置 SKU(中文注释:基于属性字典项的 name 生成)
|
||||||
if (sku) {
|
if (sku) {
|
||||||
product.sku = sku;
|
product.sku = sku;
|
||||||
} else {
|
} else {
|
||||||
// 生成 SKU
|
const attributeMap: Record<string, string> = {};
|
||||||
|
for (const a of resolvedAttributes) {
|
||||||
|
if (a?.dict?.name && a?.name) attributeMap[a.dict.name] = a.name;
|
||||||
|
}
|
||||||
product.sku = await this.templateService.render('product_sku', {
|
product.sku = await this.templateService.render('product_sku', {
|
||||||
brand: brandItem.name,
|
brand: attributeMap['brand'] || '',
|
||||||
flavor: flavorItem.name,
|
flavor: attributeMap['flavor'] || '',
|
||||||
strength: strengthItem.name,
|
strength: attributeMap['strength'] || '',
|
||||||
humidity: humidityItem.name,
|
humidity: attributeMap['humidity'] || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存产品
|
// 价格与促销价(中文注释:可选字段)
|
||||||
|
if (price !== undefined) {
|
||||||
|
product.price = Number(price);
|
||||||
|
}
|
||||||
|
const promotionPrice = (createProductDTO as any)?.promotionPrice;
|
||||||
|
if (promotionPrice !== undefined) {
|
||||||
|
product.promotionPrice = Number(promotionPrice);
|
||||||
|
}
|
||||||
|
|
||||||
return await this.productModel.save(product);
|
return await this.productModel.save(product);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,15 +284,133 @@ export class ProductService {
|
||||||
id: number,
|
id: number,
|
||||||
updateProductDTO: UpdateProductDTO
|
updateProductDTO: UpdateProductDTO
|
||||||
): Promise<Product> {
|
): Promise<Product> {
|
||||||
// 确认产品是否存在
|
// 检查产品是否存在(包含属性关系)
|
||||||
const product = await this.productModel.findOneBy({ id });
|
const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict'] });
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new Error(`产品 ID ${id} 不存在`);
|
throw new Error(`产品 ID ${id} 不存在`);
|
||||||
}
|
}
|
||||||
// 更新产品
|
|
||||||
await this.productModel.update(id, updateProductDTO);
|
// 处理基础字段更新(若传入则更新)
|
||||||
// 返回更新后的产品
|
if (updateProductDTO.name !== undefined) {
|
||||||
return await this.productModel.findOneBy({ id });
|
product.name = updateProductDTO.name;
|
||||||
|
}
|
||||||
|
if (updateProductDTO.description !== undefined) {
|
||||||
|
product.description = updateProductDTO.description;
|
||||||
|
}
|
||||||
|
if (updateProductDTO.price !== undefined) {
|
||||||
|
product.price = Number(updateProductDTO.price);
|
||||||
|
}
|
||||||
|
if ((updateProductDTO as any).promotionPrice !== undefined) {
|
||||||
|
product.promotionPrice = Number((updateProductDTO as any).promotionPrice);
|
||||||
|
}
|
||||||
|
if (updateProductDTO.sku !== undefined) {
|
||||||
|
// 校验 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 已存在,请更换后重试');
|
||||||
|
}
|
||||||
|
product.sku = newSku;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理属性更新(中文注释:若传入 attributes 则按字典名称替换对应项)
|
||||||
|
if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) {
|
||||||
|
const nextAttributes: DictItem[] = [...(product.attributes || [])];
|
||||||
|
|
||||||
|
const replaceAttr = (dictName: string, item: DictItem) => {
|
||||||
|
const idx = nextAttributes.findIndex(a => a.dict?.name === dictName);
|
||||||
|
if (idx >= 0) nextAttributes[idx] = item; else nextAttributes.push(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const attr of updateProductDTO.attributes) {
|
||||||
|
let item: DictItem | null = null;
|
||||||
|
if (attr.id) {
|
||||||
|
// 中文注释:当提供 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 信息
|
||||||
|
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 作为替换键
|
||||||
|
const dictKey = attr.dictName || item?.dict?.name;
|
||||||
|
if (!dictKey) throw new Error('无法确定字典名称用于替换属性');
|
||||||
|
replaceAttr(dictKey, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
product.attributes = nextAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存更新后的产品
|
||||||
|
const saved = await this.productModel.save(product);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:获取产品的库存组成列表(表关联版本)
|
||||||
|
async getProductComponents(productId: number): Promise<ProductStockComponent[]> {
|
||||||
|
// 条件判断:确保产品存在
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
return await this.productStockComponentModel.find({ where: { productId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:设置产品的库存组成(覆盖式,表关联版本)
|
||||||
|
async setProductComponents(
|
||||||
|
productId: number,
|
||||||
|
items: { stockId: number; quantity: number }[]
|
||||||
|
): Promise<ProductStockComponent[]> {
|
||||||
|
// 条件判断:确保产品存在
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
|
||||||
|
const validItems = (items || [])
|
||||||
|
.filter(i => i && i.stockId && i.quantity && i.quantity > 0)
|
||||||
|
.map(i => ({ stockId: Number(i.stockId), quantity: Number(i.quantity) }));
|
||||||
|
|
||||||
|
// 删除旧的组成
|
||||||
|
await this.productStockComponentModel.delete({ productId });
|
||||||
|
|
||||||
|
// 插入新的组成
|
||||||
|
const created: ProductStockComponent[] = [];
|
||||||
|
for (const i of validItems) {
|
||||||
|
const stock = await this.stockModel.findOne({ where: { id: i.stockId } });
|
||||||
|
if (!stock) throw new Error(`库存 ID ${i.stockId} 不存在`);
|
||||||
|
const comp = new ProductStockComponent();
|
||||||
|
comp.productId = productId;
|
||||||
|
comp.stockId = i.stockId;
|
||||||
|
comp.quantity = i.quantity;
|
||||||
|
comp.stock = stock;
|
||||||
|
created.push(await this.productStockComponentModel.save(comp));
|
||||||
|
}
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文注释:根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1)
|
||||||
|
async autoBindComponentsBySku(productId: number): Promise<ProductStockComponent[]> {
|
||||||
|
// 条件判断:确保产品存在
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
|
||||||
|
const stocks = await this.stockModel.find({ where: { productSku: product.sku } });
|
||||||
|
if (stocks.length === 0) return [];
|
||||||
|
|
||||||
|
for (const stock of stocks) {
|
||||||
|
// 条件判断:若已存在相同 stockId 的组成则跳过
|
||||||
|
const exist = await this.productStockComponentModel.findOne({ where: { productId, stockId: stock.id } });
|
||||||
|
if (exist) continue;
|
||||||
|
const comp = new ProductStockComponent();
|
||||||
|
comp.productId = productId;
|
||||||
|
comp.stockId = stock.id;
|
||||||
|
comp.quantity = 1; // 默认数量 1
|
||||||
|
comp.stock = stock;
|
||||||
|
await this.productStockComponentModel.save(comp);
|
||||||
|
}
|
||||||
|
return await this.getProductComponents(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
||||||
|
|
@ -523,6 +649,81 @@ export class ProductService {
|
||||||
return result.affected > 0;
|
return result.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// size 尺寸相关方法
|
||||||
|
async getSizeList(
|
||||||
|
pagination: PaginationParams,
|
||||||
|
title?: string
|
||||||
|
): Promise<SizePaginatedResponse> {
|
||||||
|
// 查找 'size' 字典(中文注释:用于尺寸)
|
||||||
|
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||||||
|
// 条件判断(中文注释:如果字典不存在则返回空分页)
|
||||||
|
if (!sizeDict) {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
...pagination,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
// 构建 where 条件(中文注释:按标题模糊搜索)
|
||||||
|
const where: any = { dict: { id: sizeDict.id } };
|
||||||
|
if (title) {
|
||||||
|
where.title = Like(`%${title}%`);
|
||||||
|
}
|
||||||
|
// 分页查询(中文注释:复用通用分页工具)
|
||||||
|
return await paginate(this.dictItemModel, { pagination, where });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSizeAll(): Promise<SizePaginatedResponse> {
|
||||||
|
// 查找 'size' 字典(中文注释:获取所有尺寸项)
|
||||||
|
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||||||
|
// 条件判断(中文注释:如果字典不存在返回空数组)
|
||||||
|
if (!sizeDict) {
|
||||||
|
return [] as any;
|
||||||
|
}
|
||||||
|
return this.dictItemModel.find({ where: { dict: { id: sizeDict.id } } }) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSize(createSizeDTO: CreateSizeDTO): Promise<DictItem> {
|
||||||
|
const { title, name } = createSizeDTO;
|
||||||
|
// 获取 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;
|
||||||
|
size.dict = sizeDict;
|
||||||
|
return await this.dictItemModel.save(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSize(id: number, updateSize: UpdateSizeDTO) {
|
||||||
|
// 先查询(中文注释:确保尺寸项存在)
|
||||||
|
const size = await this.dictItemModel.findOneBy({ id });
|
||||||
|
// 条件判断(中文注释:不存在则报错)
|
||||||
|
if (!size) {
|
||||||
|
throw new Error(`尺寸 ID ${id} 不存在`);
|
||||||
|
}
|
||||||
|
// 更新(中文注释:写入变更字段)
|
||||||
|
await this.dictItemModel.update(id, updateSize);
|
||||||
|
// 返回最新(中文注释:再次查询返回)
|
||||||
|
return await this.dictItemModel.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSize(id: number): Promise<boolean> {
|
||||||
|
// 先查询(中文注释:确保尺寸项存在)
|
||||||
|
const size = await this.dictItemModel.findOneBy({ id });
|
||||||
|
// 条件判断(中文注释:不存在则报错)
|
||||||
|
if (!size) {
|
||||||
|
throw new Error(`尺寸 ID ${id} 不存在`);
|
||||||
|
}
|
||||||
|
// 删除(中文注释:执行删除并返回受影响行数是否>0)
|
||||||
|
const result = await this.dictItemModel.delete(id);
|
||||||
|
return result.affected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async hasStrength(title: string, id?: string): Promise<boolean> {
|
async hasStrength(title: string, id?: string): Promise<boolean> {
|
||||||
const strengthDict = await this.dictModel.findOne({
|
const strengthDict = await this.dictModel.findOne({
|
||||||
|
|
@ -584,6 +785,75 @@ export class ProductService {
|
||||||
return await this.dictItemModel.save(strength);
|
return await this.dictItemModel.save(strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通用属性:分页获取指定字典的字典项
|
||||||
|
async getAttributeList(
|
||||||
|
dictName: string,
|
||||||
|
pagination: PaginationParams,
|
||||||
|
name?: string
|
||||||
|
): Promise<BrandPaginatedResponse> {
|
||||||
|
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||||
|
if (!dict) return { items: [], total: 0, ...pagination } as any;
|
||||||
|
const where: any = { dict: { id: dict.id } };
|
||||||
|
if (name) where.title = Like(`%${name}%`);
|
||||||
|
const [items, total] = await this.dictItemModel.findAndCount({
|
||||||
|
where,
|
||||||
|
skip: (pagination.current - 1) * pagination.pageSize,
|
||||||
|
take: pagination.pageSize,
|
||||||
|
order: { sort: 'ASC', id: 'DESC' },
|
||||||
|
relations: ['dict'],
|
||||||
|
});
|
||||||
|
return { items, total, ...pagination } as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用属性:获取指定字典的全部字典项
|
||||||
|
async getAttributeAll(dictName: string): Promise<DictItem[]> {
|
||||||
|
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||||
|
if (!dict) return [];
|
||||||
|
return this.dictItemModel.find({
|
||||||
|
where: { dict: { id: dict.id } },
|
||||||
|
order: { sort: 'ASC', id: 'DESC' },
|
||||||
|
relations: ['dict'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用属性:创建字典项
|
||||||
|
async createAttribute(
|
||||||
|
dictName: string,
|
||||||
|
payload: { title: string; name: string }
|
||||||
|
): Promise<DictItem> {
|
||||||
|
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||||
|
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
||||||
|
const exists = await this.dictItemModel.findOne({
|
||||||
|
where: { name: payload.name, dict: { id: dict.id } },
|
||||||
|
relations: ['dict'],
|
||||||
|
});
|
||||||
|
if (exists) throw new Error('字典项已存在');
|
||||||
|
const item = new DictItem();
|
||||||
|
item.title = payload.title;
|
||||||
|
item.name = payload.name;
|
||||||
|
item.dict = dict;
|
||||||
|
return await this.dictItemModel.save(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用属性:更新字典项
|
||||||
|
async updateAttribute(
|
||||||
|
id: number,
|
||||||
|
payload: { title?: string; name?: string }
|
||||||
|
): Promise<DictItem> {
|
||||||
|
const item = await this.dictItemModel.findOne({ where: { id } });
|
||||||
|
if (!item) throw new Error('字典项不存在');
|
||||||
|
if (payload.title !== undefined) item.title = payload.title;
|
||||||
|
if (payload.name !== undefined) item.name = payload.name;
|
||||||
|
return await this.dictItemModel.save(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用属性:删除字典项(若存在产品关联则禁止删除)
|
||||||
|
async deleteAttribute(id: number): Promise<void> {
|
||||||
|
const hasProducts = await this.hasProductsInAttribute(id);
|
||||||
|
if (hasProducts) throw new Error('当前字典项存在关联产品,无法删除');
|
||||||
|
await this.dictItemModel.delete({ id });
|
||||||
|
}
|
||||||
|
|
||||||
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
|
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
|
||||||
const strength = await this.dictItemModel.findOneBy({ id });
|
const strength = await this.dictItemModel.findOneBy({ id });
|
||||||
if (!strength) {
|
if (!strength) {
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ export class SiteService {
|
||||||
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
||||||
for (const siteConfig of sites) {
|
for (const siteConfig of sites) {
|
||||||
// 按站点名称查询是否已存在记录
|
// 按站点名称查询是否已存在记录
|
||||||
const exist = await this.siteModel.findOne({ where: { name: siteConfig.siteName } });
|
const exist = await this.siteModel.findOne({ where: { name: siteConfig.name } });
|
||||||
// 将 WpSite 字段映射为 Site 实体字段
|
// 将 WpSite 字段映射为 Site 实体字段
|
||||||
const payload: Partial<Site> = {
|
const payload: Partial<Site> = {
|
||||||
name: siteConfig.siteName,
|
name: siteConfig.name,
|
||||||
apiUrl: (siteConfig as any).wpApiUrl,
|
apiUrl: (siteConfig as any).wpApiUrl,
|
||||||
consumerKey: (siteConfig as any).consumerKey,
|
consumerKey: (siteConfig as any).consumerKey,
|
||||||
consumerSecret: (siteConfig as any).consumerSecret,
|
consumerSecret: (siteConfig as any).consumerSecret,
|
||||||
|
|
@ -66,7 +66,7 @@ export class SiteService {
|
||||||
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any;
|
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any;
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
// 按名称模糊查询
|
// 按名称模糊查询
|
||||||
if (keyword) where.siteName = Like(`%${keyword}%`);
|
if (keyword) where.name = Like(`%${keyword}%`);
|
||||||
// 按禁用状态过滤(布尔转数值)
|
// 按禁用状态过滤(布尔转数值)
|
||||||
if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0;
|
if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0;
|
||||||
if (ids) {
|
if (ids) {
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,22 @@ export class StockService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getStocksBySkus(skus: string[]) {
|
||||||
|
if (!skus || skus.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stocks = await this.stockModel
|
||||||
|
.createQueryBuilder('stock')
|
||||||
|
.select('stock.productSku', 'productSku')
|
||||||
|
.addSelect('SUM(stock.quantity)', 'totalQuantity')
|
||||||
|
.where('stock.productSku IN (:...skus)', { skus })
|
||||||
|
.groupBy('stock.productSku')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
return stocks;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新库存
|
// 更新库存
|
||||||
async updateStock(data: UpdateStockDTO) {
|
async updateStock(data: UpdateStockDTO) {
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// src/service/user.service.ts
|
// src/service/user.service.ts
|
||||||
import { Body, httpError, Inject, Provide } from '@midwayjs/core';
|
import { Body, httpError, Inject, Provide } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Like, Repository } from 'typeorm';
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from 'bcryptjs';
|
||||||
import { JwtService } from '@midwayjs/jwt';
|
import { JwtService } from '@midwayjs/jwt';
|
||||||
import { User } from '../entity/user.entity';
|
import { User } from '../entity/user.entity';
|
||||||
|
|
@ -81,7 +81,8 @@ export class UserService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUser(username: string, password: string) {
|
// 中文注释:新增用户(支持可选备注)
|
||||||
|
async addUser(username: string, password: string, remark?: string) {
|
||||||
const existingUser = await this.userModel.findOne({
|
const existingUser = await this.userModel.findOne({
|
||||||
where: { username },
|
where: { username },
|
||||||
});
|
});
|
||||||
|
|
@ -92,14 +93,37 @@ export class UserService {
|
||||||
const user = this.userModel.create({
|
const user = this.userModel.create({
|
||||||
username,
|
username,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
// 中文注释:备注字段赋值(若提供)
|
||||||
|
...(remark ? { remark } : {}),
|
||||||
});
|
});
|
||||||
return this.userModel.save(user);
|
return this.userModel.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listUsers(current: number, pageSize: number) {
|
// 中文注释:用户列表支持分页与备注模糊查询(以及可选的布尔过滤)
|
||||||
|
async listUsers(
|
||||||
|
current: number,
|
||||||
|
pageSize: number,
|
||||||
|
filters: {
|
||||||
|
remark?: string;
|
||||||
|
username?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
isSuper?: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
// 条件判断:构造 where 条件
|
||||||
|
const where: Record<string, any> = {};
|
||||||
|
if (filters.username) where.username = Like(`%${filters.username}%`); // 中文注释:用户名精确匹配(如需模糊可改为 Like)
|
||||||
|
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 中文注释:按启用状态过滤
|
||||||
|
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 中文注释:按超管过滤
|
||||||
|
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 中文注释:按管理员过滤
|
||||||
|
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 中文注释:备注模糊搜索
|
||||||
|
|
||||||
const [items, total] = await this.userModel.findAndCount({
|
const [items, total] = await this.userModel.findAndCount({
|
||||||
|
where,
|
||||||
skip: (current - 1) * pageSize,
|
skip: (current - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
|
order: { id: 'DESC' },
|
||||||
});
|
});
|
||||||
return { items, total, current, pageSize };
|
return { items, total, current, pageSize };
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +137,48 @@ export class UserService {
|
||||||
return this.userModel.save(user);
|
return this.userModel.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文注释:更新用户信息(支持用户名唯一校验与可选密码修改)
|
||||||
|
async updateUser(
|
||||||
|
userId: number,
|
||||||
|
payload: {
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
isSuper?: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
permissions?: string[];
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// 条件判断:查询用户是否存在
|
||||||
|
const user = await this.userModel.findOne({ where: { id: userId } });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件判断:若提供了新用户名且与原用户名不同,校验唯一性
|
||||||
|
if (payload.username && payload.username !== user.username) {
|
||||||
|
const exist = await this.userModel.findOne({ where: { username: payload.username } });
|
||||||
|
if (exist) throw new Error('用户名已存在');
|
||||||
|
user.username = payload.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件判断:若提供密码则进行加密存储
|
||||||
|
if (payload.password) {
|
||||||
|
user.password = await bcrypt.hash(payload.password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
||||||
|
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
||||||
|
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
||||||
|
if (Array.isArray(payload.permissions)) user.permissions = payload.permissions;
|
||||||
|
|
||||||
|
// 条件判断:更新备注(若提供则覆盖)
|
||||||
|
if (typeof payload.remark === 'string') user.remark = payload.remark;
|
||||||
|
|
||||||
|
// 保存更新
|
||||||
|
return await this.userModel.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
async getUser(userId: number) {
|
async getUser(userId: number) {
|
||||||
return plainToInstance(
|
return plainToInstance(
|
||||||
User,
|
User,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue