feat: 添加区域坐标功能并重构产品属性管理
- 在区域实体中添加经纬度字段,支持坐标功能 - 重构产品属性管理,使用ID关联替代对象嵌套 - 新增产品尺寸管理功能及相关API - 添加库存查询接口,支持按SKU批量查询 - 统一站点名称字段从siteName改为name - 添加数据库迁移指南和API文档 - 优化实体加载方式,使用通配符匹配
This commit is contained in:
parent
fb8509b5f5
commit
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",
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,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: '',
|
||||||
// },
|
// },
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,25 @@ import {
|
||||||
CreateFlavorsDTO,
|
CreateFlavorsDTO,
|
||||||
CreateProductDTO,
|
CreateProductDTO,
|
||||||
CreateStrengthDTO,
|
CreateStrengthDTO,
|
||||||
|
CreateSizeDTO,
|
||||||
QueryBrandDTO,
|
QueryBrandDTO,
|
||||||
QueryFlavorsDTO,
|
QueryFlavorsDTO,
|
||||||
QueryProductDTO,
|
QueryProductDTO,
|
||||||
QueryStrengthDTO,
|
QueryStrengthDTO,
|
||||||
|
QuerySizeDTO,
|
||||||
UpdateBrandDTO,
|
UpdateBrandDTO,
|
||||||
UpdateFlavorsDTO,
|
UpdateFlavorsDTO,
|
||||||
UpdateProductDTO,
|
UpdateProductDTO,
|
||||||
UpdateStrengthDTO,
|
UpdateStrengthDTO,
|
||||||
|
UpdateSizeDTO,
|
||||||
} from '../dto/product.dto';
|
} from '../dto/product.dto';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import {
|
import {
|
||||||
BooleanRes,
|
BooleanRes,
|
||||||
ProductBrandListRes,
|
ProductBrandListRes,
|
||||||
ProductBrandRes,
|
ProductBrandRes,
|
||||||
|
ProductSizeListRes,
|
||||||
|
ProductSizeRes,
|
||||||
ProductListRes,
|
ProductListRes,
|
||||||
ProductRes,
|
ProductRes,
|
||||||
ProductsRes,
|
ProductsRes,
|
||||||
|
|
@ -406,4 +411,93 @@ export class ProductController {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// size 路由与增删改查
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/sizeAll')
|
||||||
|
async getSizeAll() {
|
||||||
|
try {
|
||||||
|
// 中文注释:获取所有尺寸项
|
||||||
|
const data = await this.productService.getSizeAll();
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: ProductSizeListRes })
|
||||||
|
@Get('/size')
|
||||||
|
async getSize(@Query() query: QuerySizeDTO) {
|
||||||
|
// 中文注释:解析分页与关键字
|
||||||
|
const { current = 1, pageSize = 10, name } = query;
|
||||||
|
try {
|
||||||
|
// 中文注释:分页查询尺寸列表
|
||||||
|
const data = await this.productService.getSizeList(
|
||||||
|
{ current, pageSize },
|
||||||
|
name
|
||||||
|
);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: ProductSizeRes })
|
||||||
|
@Post('/size')
|
||||||
|
async createSize(@Body() sizeData: CreateSizeDTO) {
|
||||||
|
try {
|
||||||
|
// 条件判断(中文注释:唯一性校验,禁止重复)
|
||||||
|
const hasSize = await this.productService.hasAttribute(
|
||||||
|
'size',
|
||||||
|
sizeData.name
|
||||||
|
);
|
||||||
|
if (hasSize) {
|
||||||
|
return errorResponse('尺寸已存在');
|
||||||
|
}
|
||||||
|
// 调用服务创建(中文注释:新增尺寸项)
|
||||||
|
const data = await this.productService.createSize(sizeData);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: ProductSizeRes })
|
||||||
|
@Put('/size/:id')
|
||||||
|
async updateSize(
|
||||||
|
@Param('id') id: number,
|
||||||
|
@Body() sizeData: UpdateSizeDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 条件判断(中文注释:唯一性校验,排除自身)
|
||||||
|
const hasSize = await this.productService.hasAttribute(
|
||||||
|
'size',
|
||||||
|
sizeData.name,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
if (hasSize) {
|
||||||
|
return errorResponse('尺寸已存在');
|
||||||
|
}
|
||||||
|
// 调用服务更新(中文注释:提交变更)
|
||||||
|
const data = await this.productService.updateSize(id, sizeData);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
|
@Del('/size/:id')
|
||||||
|
async deleteSize(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
// 条件判断(中文注释:若有商品关联则不可删除)
|
||||||
|
const hasProducts = await this.productService.hasProductsInAttribute(id);
|
||||||
|
if (hasProducts) throw new Error('该尺寸下有商品,无法删除');
|
||||||
|
// 调用服务删除(中文注释:返回是否成功)
|
||||||
|
const data = await this.productService.deleteSize(id);
|
||||||
|
return successResponse(data);
|
||||||
|
} 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 || '获取失败');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,10 @@ export class UpdateDictItemDTO {
|
||||||
|
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
title?: string; // 字典项标题 (可选)
|
title?: string; // 字典项标题 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow(null))
|
||||||
|
value?: string; // 字典项值 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow(null))
|
||||||
|
titleCN?: 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,32 +21,17 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '品牌', type: DictItemDTO })
|
@ApiProperty({ description: '品牌 ID', type: 'number' })
|
||||||
@Rule(
|
@Rule(RuleType.number().required())
|
||||||
RuleType.object().keys({
|
brandId: number;
|
||||||
title: RuleType.string().required(),
|
|
||||||
name: RuleType.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
brand: DictItemDTO;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '规格', type: DictItemDTO })
|
@ApiProperty({ description: '规格 ID', type: 'number' })
|
||||||
@Rule(
|
@Rule(RuleType.number().required())
|
||||||
RuleType.object().keys({
|
strengthId: number;
|
||||||
title: RuleType.string().required(),
|
|
||||||
name: RuleType.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
strength: DictItemDTO;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '口味', type: DictItemDTO })
|
@ApiProperty({ description: '口味 ID', type: 'number' })
|
||||||
@Rule(
|
@Rule(RuleType.number().required())
|
||||||
RuleType.object().keys({
|
flavorsId: number;
|
||||||
title: RuleType.string().required(),
|
|
||||||
name: RuleType.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
flavor: DictItemDTO;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
|
|
@ -213,6 +188,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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ export class Product {
|
||||||
@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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,7 @@ 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';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class ProductService {
|
export class ProductService {
|
||||||
|
|
@ -35,6 +39,9 @@ export class ProductService {
|
||||||
@Inject()
|
@Inject()
|
||||||
templateService: TemplateService;
|
templateService: TemplateService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
stockService: StockService;
|
||||||
|
|
||||||
@InjectEntityModel(Product)
|
@InjectEntityModel(Product)
|
||||||
productModel: Repository<Product>;
|
productModel: Repository<Product>;
|
||||||
|
|
||||||
|
|
@ -157,6 +164,16 @@ export class ProductService {
|
||||||
|
|
||||||
const [items, total] = await qb.getManyAndCount();
|
const [items, total] = await qb.getManyAndCount();
|
||||||
|
|
||||||
|
// 获取所有 SKU 的库存信息
|
||||||
|
const skus = items.map(item => item.sku).filter(Boolean);
|
||||||
|
const stocks = await this.stockService.getStocksBySkus(skus);
|
||||||
|
|
||||||
|
// 将库存信息映射到 SKU
|
||||||
|
const stockMap = stocks.reduce((map, stock) => {
|
||||||
|
map[stock.productSku] = stock.totalQuantity;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
// 格式化返回的数据
|
// 格式化返回的数据
|
||||||
const formattedItems = items.map(product => {
|
const formattedItems = items.map(product => {
|
||||||
const getAttributeTitle = (dictName: string) =>
|
const getAttributeTitle = (dictName: string) =>
|
||||||
|
|
@ -169,8 +186,13 @@ export class ProductService {
|
||||||
description: product.description,
|
description: product.description,
|
||||||
humidity: getAttributeTitle('humidity'),
|
humidity: getAttributeTitle('humidity'),
|
||||||
sku: product.sku,
|
sku: product.sku,
|
||||||
|
stock: stockMap[product.sku] || 0, // 使用映射的库存或默认为 0
|
||||||
|
price: product.price,
|
||||||
|
promotionPrice: product.promotionPrice,
|
||||||
|
source: product.source, // 中文注释:补充返回产品来源字段
|
||||||
createdAt: product.createdAt,
|
createdAt: product.createdAt,
|
||||||
updatedAt: product.updatedAt,
|
updatedAt: product.updatedAt,
|
||||||
|
attributes: product.attributes,
|
||||||
brandName: getAttributeTitle('brand'),
|
brandName: getAttributeTitle('brand'),
|
||||||
flavorsName: getAttributeTitle('flavor'),
|
flavorsName: getAttributeTitle('flavor'),
|
||||||
strengthName: getAttributeTitle('strength'),
|
strengthName: getAttributeTitle('strength'),
|
||||||
|
|
@ -213,25 +235,19 @@ 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, brandId, flavorsId, strengthId, humidity, sku, price } =
|
||||||
createProductDTO;
|
createProductDTO;
|
||||||
|
|
||||||
// 获取或创建品牌、口味、规格
|
// 获取或创建品牌、口味、规格
|
||||||
const brandItem = await this.getOrCreateDictItem(
|
const brandItem = await this.dictItemModel.findOne({ where: { id: brandId } });
|
||||||
'brand',
|
if (!brandItem) throw new Error('品牌不存在');
|
||||||
brand.title,
|
|
||||||
brand.name
|
const flavorItem = await this.dictItemModel.findOne({ where: { id: flavorsId } });
|
||||||
);
|
if (!flavorItem) throw new Error('口味不存在');
|
||||||
const flavorItem = await this.getOrCreateDictItem(
|
|
||||||
'flavor',
|
const strengthItem = await this.dictItemModel.findOne({ where: { id: strengthId } });
|
||||||
flavor.title,
|
if (!strengthItem) throw new Error('规格不存在');
|
||||||
flavor.name
|
|
||||||
);
|
|
||||||
const strengthItem = await this.getOrCreateDictItem(
|
|
||||||
'strength',
|
|
||||||
strength.title,
|
|
||||||
strength.name
|
|
||||||
);
|
|
||||||
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
|
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
|
||||||
|
|
||||||
// 检查具有完全相同属性组合的产品是否已存在
|
// 检查具有完全相同属性组合的产品是否已存在
|
||||||
|
|
@ -268,6 +284,15 @@ export class ProductService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 价格与促销价
|
||||||
|
if (price !== undefined) {
|
||||||
|
product.price = Number(price);
|
||||||
|
}
|
||||||
|
const promotionPrice = (createProductDTO as any)?.promotionPrice;
|
||||||
|
if (promotionPrice !== undefined) {
|
||||||
|
product.promotionPrice = Number(promotionPrice);
|
||||||
|
}
|
||||||
|
|
||||||
// 保存产品
|
// 保存产品
|
||||||
return await this.productModel.save(product);
|
return await this.productModel.save(product);
|
||||||
}
|
}
|
||||||
|
|
@ -276,15 +301,75 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理属性更新(品牌/口味/强度/干湿)
|
||||||
|
const nextAttributes: DictItem[] = [...(product.attributes || [])];
|
||||||
|
|
||||||
|
// 根据 dict.name 查找或替换已有属性
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 品牌
|
||||||
|
if (updateProductDTO.brandId !== undefined) {
|
||||||
|
const brandItem = await this.dictItemModel.findOne({ where: { id: updateProductDTO.brandId }, relations: ['dict'] });
|
||||||
|
if (!brandItem) throw new Error('品牌不存在');
|
||||||
|
replaceAttr('brand', brandItem);
|
||||||
|
}
|
||||||
|
// 口味
|
||||||
|
if (updateProductDTO.flavorsId !== undefined) {
|
||||||
|
const flavorItem = await this.dictItemModel.findOne({ where: { id: updateProductDTO.flavorsId }, relations: ['dict'] });
|
||||||
|
if (!flavorItem) throw new Error('口味不存在');
|
||||||
|
replaceAttr('flavor', flavorItem);
|
||||||
|
}
|
||||||
|
// 强度
|
||||||
|
if (updateProductDTO.strengthId !== undefined) {
|
||||||
|
const strengthItem = await this.dictItemModel.findOne({ where: { id: updateProductDTO.strengthId }, relations: ['dict'] });
|
||||||
|
if (!strengthItem) throw new Error('规格不存在');
|
||||||
|
replaceAttr('strength', strengthItem);
|
||||||
|
}
|
||||||
|
// 干湿(按 title 获取或创建)
|
||||||
|
if (updateProductDTO.humidity !== undefined) {
|
||||||
|
const humidityItem = await this.getOrCreateDictItem('humidity', updateProductDTO.humidity);
|
||||||
|
replaceAttr('humidity', humidityItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
product.attributes = nextAttributes;
|
||||||
|
|
||||||
|
// 保存更新后的产品
|
||||||
|
const saved = await this.productModel.save(product);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
||||||
|
|
@ -523,6 +608,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({
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue