diff --git a/area-api-doc.md b/area-api-doc.md
new file mode 100644
index 0000000..8eb579d
--- /dev/null
+++ b/area-api-doc.md
@@ -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(`${area.name}`);
+ }
+ });
+ ```
+
+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可能返回的错误信息:
+
+- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
+- `区域不存在`: 当尝试更新或删除不存在的区域时
+- `权限错误`: 当请求缺少有效的授权令牌时
\ No newline at end of file
diff --git a/migration-guide.md b/migration-guide.md
new file mode 100644
index 0000000..ba5339e
--- /dev/null
+++ b/migration-guide.md
@@ -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开始为区域添加坐标信息
\ No newline at end of file
diff --git a/package.json b/package.json
index 169ab4b..c40ff7a 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,9 @@
"build": "mwtsc --cleanOutDir",
"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",
- "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": {
"type": "git",
diff --git a/src/config/config.default.ts b/src/config/config.default.ts
index a5f2c8c..d1bb37a 100644
--- a/src/config/config.default.ts
+++ b/src/config/config.default.ts
@@ -111,7 +111,7 @@ export default {
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
- // siteName: 'Local',
+ // name: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: '',
// },
diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts
index 1313485..8c3cdf5 100644
--- a/src/controller/product.controller.ts
+++ b/src/controller/product.controller.ts
@@ -17,20 +17,25 @@ import {
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
+ CreateSizeDTO,
QueryBrandDTO,
QueryFlavorsDTO,
QueryProductDTO,
QueryStrengthDTO,
+ QuerySizeDTO,
UpdateBrandDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
+ UpdateSizeDTO,
} from '../dto/product.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
ProductBrandListRes,
ProductBrandRes,
+ ProductSizeListRes,
+ ProductSizeRes,
ProductListRes,
ProductRes,
ProductsRes,
@@ -406,4 +411,93 @@ export class ProductController {
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);
+ }
+ }
}
diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts
index 5c848d5..02cf312 100644
--- a/src/controller/site.controller.ts
+++ b/src/controller/site.controller.ts
@@ -15,7 +15,7 @@ export class SiteController {
async all() {
try {
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) {
return errorResponse(error?.message || '获取失败');
}
diff --git a/src/db/datasource.ts b/src/db/datasource.ts
index a05ac66..3f996fd 100644
--- a/src/db/datasource.ts
+++ b/src/db/datasource.ts
@@ -1,41 +1,6 @@
import { DataSource, DataSourceOptions } from 'typeorm';
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 = {
type: 'mysql',
@@ -46,44 +11,7 @@ const options: DataSourceOptions & SeederOptions = {
database: 'inventory',
synchronize: false,
logging: true,
- entities: [
- 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,
- ],
+ entities: [__dirname + '/../entity/*.ts'],
migrations: ['src/db/migrations/**/*.ts'],
seeds: ['src/db/seeds/**/*.ts'],
};
diff --git a/src/db/migrations/1764294088896-Area.ts b/src/db/migrations/1764294088896-Area.ts
index 188c35a..e2948bb 100644
--- a/src/db/migrations/1764294088896-Area.ts
+++ b/src/db/migrations/1764294088896-Area.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 \`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(`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 `source` int NOT NULL DEFAULT '0'`);
// 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 \`product\` DROP COLUMN \`source\``);
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_926a14ac4c91f38792831acd2a\` ON \`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 INDEX \`IDX_644ffaf8fbde4db798cb47712f\` ON \`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\`)`);
}
}
diff --git a/src/db/migrations/1764299629279-ProductStock.ts b/src/db/migrations/1764299629279-ProductStock.ts
new file mode 100644
index 0000000..9767e45
--- /dev/null
+++ b/src/db/migrations/1764299629279-ProductStock.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ProductStock1764299629279 implements MigrationInterface {
+ name = 'ProductStock1764299629279'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ 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 {
+ await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP FOREIGN KEY \`FK_ca48e4bce0bb8cecd24cc8081e5\``);
+ await queryRunner.query(`DROP TABLE \`order_item_original\``);
+ }
+
+}
diff --git a/src/dto/area.dto.ts b/src/dto/area.dto.ts
index acf0d05..9bb8b82 100644
--- a/src/dto/area.dto.ts
+++ b/src/dto/area.dto.ts
@@ -7,6 +7,14 @@ export class CreateAreaDTO {
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
@Rule(RuleType.string().required())
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: '欧洲' })
@Rule(RuleType.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;
}
// 查询区域的数据传输对象
diff --git a/src/dto/dict.dto.ts b/src/dto/dict.dto.ts
index b84ac8e..85c8b02 100644
--- a/src/dto/dict.dto.ts
+++ b/src/dto/dict.dto.ts
@@ -26,6 +26,9 @@ export class CreateDictItemDTO {
@Rule(RuleType.string().required())
title: string; // 字典项标题
+ @Rule(RuleType.string().allow(null))
+ titleCN?: string; // 字典项中文标题 (可选)
+
@Rule(RuleType.number().required())
dictId: number; // 所属字典的ID
}
@@ -37,4 +40,10 @@ export class UpdateDictItemDTO {
@Rule(RuleType.string())
title?: string; // 字典项标题 (可选)
+
+ @Rule(RuleType.string().allow(null))
+ value?: string; // 字典项值 (可选)
+
+ @Rule(RuleType.string().allow(null))
+ titleCN?: string; // 字典项中文标题 (可选)
}
diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts
index 2fdfb22..2419073 100644
--- a/src/dto/product.dto.ts
+++ b/src/dto/product.dto.ts
@@ -1,16 +1,6 @@
import { ApiProperty } from '@midwayjs/swagger';
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 用于创建产品
*/
@@ -31,32 +21,17 @@ export class CreateProductDTO {
@Rule(RuleType.string())
sku?: string;
- @ApiProperty({ description: '品牌', type: DictItemDTO })
- @Rule(
- RuleType.object().keys({
- title: RuleType.string().required(),
- name: RuleType.string(),
- })
- )
- brand: DictItemDTO;
+ @ApiProperty({ description: '品牌 ID', type: 'number' })
+ @Rule(RuleType.number().required())
+ brandId: number;
- @ApiProperty({ description: '规格', type: DictItemDTO })
- @Rule(
- RuleType.object().keys({
- title: RuleType.string().required(),
- name: RuleType.string(),
- })
- )
- strength: DictItemDTO;
+ @ApiProperty({ description: '规格 ID', type: 'number' })
+ @Rule(RuleType.number().required())
+ strengthId: number;
- @ApiProperty({ description: '口味', type: DictItemDTO })
- @Rule(
- RuleType.object().keys({
- title: RuleType.string().required(),
- name: RuleType.string(),
- })
- )
- flavor: DictItemDTO;
+ @ApiProperty({ description: '口味 ID', type: 'number' })
+ @Rule(RuleType.number().required())
+ flavorsId: number;
@ApiProperty()
@Rule(RuleType.string())
@@ -213,6 +188,41 @@ export class QueryStrengthDTO {
name: string; // 搜索关键字(支持模糊查询)
}
+// size 新增 DTO
+export class CreateSizeDTO {
+ @ApiProperty({ example: '6', description: '尺寸名称', required: false })
+ @Rule(RuleType.string())
+ title?: string;
+
+ @ApiProperty({ example: '6', description: '尺寸唯一标识', required: true })
+ @Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
+ name: string;
+}
+
+export class UpdateSizeDTO {
+ @ApiProperty({ example: '6', description: '尺寸名称' })
+ @Rule(RuleType.string())
+ title: string;
+
+ @ApiProperty({ example: '6', description: '尺寸唯一标识' })
+ @Rule(RuleType.string())
+ name: string;
+}
+
+export class QuerySizeDTO {
+ @ApiProperty({ example: '1', description: '页码' })
+ @Rule(RuleType.number())
+ current: number; // 页码
+
+ @ApiProperty({ example: '10', description: '每页大小' })
+ @Rule(RuleType.number())
+ pageSize: number; // 每页大小
+
+ @ApiProperty({ example: '6', description: '关键字' })
+ @Rule(RuleType.string())
+ name: string; // 搜索关键字(支持模糊查询)
+}
+
export class SkuItemDTO {
@ApiProperty({ description: '产品 ID' })
productId: number;
diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts
index 6ee01ee..1c43649 100644
--- a/src/dto/reponse.dto.ts
+++ b/src/dto/reponse.dto.ts
@@ -63,9 +63,20 @@ export class ProductStrengthListRes extends SuccessWrapper(
) {}
//产品规格返所有数据
export class ProductStrengthAllRes extends SuccessArrayWrapper(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(
WpProductDTO
diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts
index 17fcdd1..39f4f6f 100644
--- a/src/dto/site.dto.ts
+++ b/src/dto/site.dto.ts
@@ -20,7 +20,7 @@ export class SiteConfig {
@ApiProperty({ description: '站点名' })
@Rule(RuleType.string())
- siteName: string;
+ name: string;
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
@@ -39,7 +39,7 @@ export class CreateSiteDTO {
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string())
- siteName: string;
+ name: string;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
@@ -54,7 +54,7 @@ export class UpdateSiteDTO {
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string().optional())
- siteName?: string;
+ name?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
diff --git a/src/entity/area.entity.ts b/src/entity/area.entity.ts
index 51caf17..7afaf34 100644
--- a/src/entity/area.entity.ts
+++ b/src/entity/area.entity.ts
@@ -17,6 +17,14 @@ export class Area {
@Column({ unique: true })
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({
example: '2022-12-12 11:11:11',
description: '创建时间',
diff --git a/src/entity/dict_item.entity.ts b/src/entity/dict_item.entity.ts
index 61fe9b2..c53b21a 100644
--- a/src/entity/dict_item.entity.ts
+++ b/src/entity/dict_item.entity.ts
@@ -25,7 +25,9 @@ export class DictItem {
// 字典项名称
@Column({ comment: '字典项显示名称' })
title: string;
-
+ // 目前没有单独做国际化, 所以这里先添加 titleCN 用来标注
+ @Column({ comment: '字典项中文名称', nullable: true })
+ titleCN: string;
// 唯一标识
@Column({ unique: true, comment: '字典唯一标识名称' })
name: string;
diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts
index 181642f..a718cf5 100644
--- a/src/entity/product.entity.ts
+++ b/src/entity/product.entity.ts
@@ -52,6 +52,10 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number;
+ @ApiProperty({ description: '库存', example: 100 })
+ @Column({ default: 0 })
+ stock: number;
+
@ManyToMany(() => DictItem, {
cascade: true,
})
diff --git a/src/interface.ts b/src/interface.ts
index f7b94f9..e000403 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -10,7 +10,7 @@ export interface WpSite {
wpApiUrl: string;
consumerKey: string;
consumerSecret: string;
- siteName: string;
+ name: string;
email: string;
emailPswd: string;
}
diff --git a/src/service/area.service.ts b/src/service/area.service.ts
index e9c9120..fbe2eac 100644
--- a/src/service/area.service.ts
+++ b/src/service/area.service.ts
@@ -22,6 +22,12 @@ export class AreaService {
}
const area = new Area();
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);
}
@@ -43,6 +49,12 @@ export class AreaService {
}
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);
}
diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts
index a79503d..36d9232 100644
--- a/src/service/logistics.service.ts
+++ b/src/service/logistics.service.ts
@@ -561,11 +561,11 @@ export class LogisticsService {
// 从数据库批量获取站点信息,构建映射以避免 N+1 查询
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
- const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName]));
+ const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name]));
return orders.map(order => ({
...order,
- siteName: siteMap.get(order.siteId) || '',
+ name: siteMap.get(order.siteId) || '',
}));
}
diff --git a/src/service/order.service.ts b/src/service/order.service.ts
index bcd787b..9fd08f1 100644
--- a/src/service/order.service.ts
+++ b/src/service/order.service.ts
@@ -1350,7 +1350,7 @@ export class OrderService {
return {
...order,
- siteName: site?.name,
+ name: site?.name,
// Site 实体无邮箱字段,这里返回空字符串保持兼容
email: '',
items,
@@ -1418,11 +1418,11 @@ export class OrderService {
// 批量获取订单涉及的站点名称,避免使用配置文件
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
- const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName]));
+ const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name]));
return orders.map(order => ({
externalOrderId: order.externalOrderId,
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 } });
if (!order) throw new Error(`订单 ${id}不存在`);
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) {
await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.CANCEL,
diff --git a/src/service/product.service.ts b/src/service/product.service.ts
index 658a167..2ecc4e0 100644
--- a/src/service/product.service.ts
+++ b/src/service/product.service.ts
@@ -8,16 +8,19 @@ import {
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
+ CreateSizeDTO,
UpdateBrandDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
+ UpdateSizeDTO,
} from '../dto/product.dto';
import {
BrandPaginatedResponse,
FlavorsPaginatedResponse,
ProductPaginatedResponse,
StrengthPaginatedResponse,
+ SizePaginatedResponse,
} from '../dto/reponse.dto';
import { InjectEntityModel } from '@midwayjs/typeorm';
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 { Context } from '@midwayjs/koa';
import { TemplateService } from './template.service';
+import { StockService } from './stock.service';
@Provide()
export class ProductService {
@@ -35,6 +39,9 @@ export class ProductService {
@Inject()
templateService: TemplateService;
+ @Inject()
+ stockService: StockService;
+
@InjectEntityModel(Product)
productModel: Repository;
@@ -157,6 +164,16 @@ export class ProductService {
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 getAttributeTitle = (dictName: string) =>
@@ -169,8 +186,13 @@ export class ProductService {
description: product.description,
humidity: getAttributeTitle('humidity'),
sku: product.sku,
+ stock: stockMap[product.sku] || 0, // 使用映射的库存或默认为 0
+ price: product.price,
+ promotionPrice: product.promotionPrice,
+ source: product.source, // 中文注释:补充返回产品来源字段
createdAt: product.createdAt,
updatedAt: product.updatedAt,
+ attributes: product.attributes,
brandName: getAttributeTitle('brand'),
flavorsName: getAttributeTitle('flavor'),
strengthName: getAttributeTitle('strength'),
@@ -213,25 +235,19 @@ export class ProductService {
}
async createProduct(createProductDTO: CreateProductDTO): Promise {
- const { name, description, brand, flavor, strength, humidity, sku } =
+ const { name, description, brandId, flavorsId, strengthId, humidity, sku, price } =
createProductDTO;
// 获取或创建品牌、口味、规格
- const brandItem = await this.getOrCreateDictItem(
- 'brand',
- 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 brandItem = await this.dictItemModel.findOne({ where: { id: brandId } });
+ if (!brandItem) throw new Error('品牌不存在');
+
+ const flavorItem = await this.dictItemModel.findOne({ where: { id: flavorsId } });
+ if (!flavorItem) throw new Error('口味不存在');
+
+ const strengthItem = await this.dictItemModel.findOne({ where: { id: strengthId } });
+ if (!strengthItem) throw new Error('规格不存在');
+
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);
}
@@ -276,15 +301,75 @@ export class ProductService {
id: number,
updateProductDTO: UpdateProductDTO
): Promise {
- // 确认产品是否存在
- const product = await this.productModel.findOneBy({ id });
+ // 检查产品是否存在(包含属性关系)
+ const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict'] });
if (!product) {
throw new Error(`产品 ID ${id} 不存在`);
}
- // 更新产品
- await this.productModel.update(id, updateProductDTO);
- // 返回更新后的产品
- return await this.productModel.findOneBy({ id });
+
+ // 处理基础字段更新(若传入则更新)
+ if (updateProductDTO.name !== undefined) {
+ 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 {
@@ -523,6 +608,81 @@ export class ProductService {
return result.affected > 0;
}
+ // size 尺寸相关方法
+ async getSizeList(
+ pagination: PaginationParams,
+ title?: string
+ ): Promise {
+ // 查找 '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 {
+ // 查找 '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 {
+ 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 {
+ // 先查询(中文注释:确保尺寸项存在)
+ 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 {
const strengthDict = await this.dictModel.findOne({
diff --git a/src/service/site.service.ts b/src/service/site.service.ts
index 0075ccc..edd2d83 100644
--- a/src/service/site.service.ts
+++ b/src/service/site.service.ts
@@ -15,10 +15,10 @@ export class SiteService {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
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 实体字段
const payload: Partial = {
- name: siteConfig.siteName,
+ name: siteConfig.name,
apiUrl: (siteConfig as any).wpApiUrl,
consumerKey: (siteConfig as any).consumerKey,
consumerSecret: (siteConfig as any).consumerSecret,
@@ -66,7 +66,7 @@ export class SiteService {
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as 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 (ids) {
diff --git a/src/service/stock.service.ts b/src/service/stock.service.ts
index c730269..0525c8c 100644
--- a/src/service/stock.service.ts
+++ b/src/service/stock.service.ts
@@ -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) {
const {