Compare commits

...

8 Commits

Author SHA1 Message Date
tikkhun 58004dd091 feat(用户): 添加备注字段并增强用户数据安全性
在用户实体中添加可选的备注字段
修改用户服务层和控制器以支持备注字段的增删改查
在所有用户数据返回接口中移除密码字段以增强安全性
2025-11-29 10:07:31 +08:00
tikkhun 7b3c7540d7 feat(用户): 添加用户信息更新接口
新增用户信息更新功能,支持修改用户名、密码、权限和角色字段
添加用户名唯一性校验和密码加密处理
2025-11-29 09:23:52 +08:00
tikkhun 6855b13ed7 feat(实体): 添加ProductStockComponent实体到默认配置中 2025-11-28 18:59:21 +08:00
tikkhun fdf2819b3b feat(产品): 实现产品库存组成功能
添加产品库存组成相关实体、DTO和服务方法
- 新增ProductStockComponent实体表示库存组成关系
- 添加获取、设置和自动绑定库存组成的API接口
- 实现库存组成的CRUD操作逻辑
2025-11-28 18:40:11 +08:00
tikkhun a7d5db33f3 fix(product): 修复属性更新时字典名称处理逻辑
当更新产品属性时,调整字典名称的处理逻辑:
1. 当提供 id 时不强制要求 dictName
2. 未提供 id 时需要 dictName 和 title/name
3. 使用传入的 dictName 或查询到的 item.dict.name 作为替换键
2025-11-28 18:11:21 +08:00
tikkhun 64b8468df8 feat(产品): 重构产品属性管理为通用字典项实现
重构产品相关的品牌、口味、规格和尺寸属性管理,改为基于通用字典项的实现方式:
1. 新增 AttributeInputDTO 用于统一处理属性输入
2. 实现通用的字典项增删改查接口
3. 保留旧接口作为兼容层
4. 优化产品创建和更新逻辑以支持新属性结构
2025-11-28 17:59:48 +08:00
tikkhun bc575840b2 refactor(产品服务): 重构产品数据格式化逻辑,直接返回字典对象
将品牌/口味/规格等属性从返回标题改为直接返回完整的 DictItem 对象
保留原 attributes 列表以便前端灵活使用
2025-11-28 16:58:53 +08:00
tikkhun 366fd93dde feat: 添加区域坐标功能并重构产品属性管理
- 在区域实体中添加经纬度字段,支持坐标功能
- 重构产品属性管理,使用ID关联替代对象嵌套
- 新增产品尺寸管理功能及相关API
- 添加库存查询接口,支持按SKU批量查询
- 统一站点名称字段从siteName改为name
- 添加数据库迁移指南和API文档
- 优化实体加载方式,使用通配符匹配
2025-11-28 16:58:14 +08:00
28 changed files with 1307 additions and 383 deletions

254
area-api-doc.md Normal file
View File

@ -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可能返回的错误信息
- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
- `区域不存在`: 当尝试更新或删除不存在的区域时
- `权限错误`: 当请求缺少有效的授权令牌时

49
migration-guide.md Normal file
View File

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

View File

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

View File

@ -35,6 +35,7 @@ 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';
import { ProductStockComponent } from '../entity/product_stock_component.entity';
import DictSeeder from '../db/seeds/dict.seeder';
export default {
@ -44,6 +45,7 @@ export default {
default: {
entities: [
Product,
ProductStockComponent,
WpProduct,
Variation,
User,
@ -111,7 +113,7 @@ export default {
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
// siteName: 'Local',
// name: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: '',
// },

View File

@ -11,30 +11,9 @@ import {
} from '@midwayjs/core';
import { ProductService } from '../service/product.service';
import { errorResponse, successResponse } from '../utils/response.util';
import {
BatchSetSkuDTO,
CreateBrandDTO,
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
QueryBrandDTO,
QueryFlavorsDTO,
QueryProductDTO,
QueryStrengthDTO,
UpdateBrandDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
} from '../dto/product.dto';
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
ProductBrandListRes,
ProductBrandRes,
ProductListRes,
ProductRes,
ProductsRes,
} from '../dto/reponse.dto';
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
@Controller('/product')
export class ProductController {
@ -93,9 +72,7 @@ export class ProductController {
}
}
@ApiOkResponse({
type: ProductRes,
})
@ApiOkResponse({ type: ProductRes })
@Post('/')
async createProduct(@Body() productData: CreateProductDTO) {
try {
@ -106,14 +83,9 @@ export class ProductController {
}
}
@ApiOkResponse({
type: ProductRes,
})
@ApiOkResponse({ type: ProductRes })
@Put('/:id')
async updateProduct(
@Param('id') id: number,
@Body() productData: UpdateProductDTO
) {
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
try {
const data = this.productService.updateProduct(id, productData);
return successResponse(data);
@ -122,14 +94,9 @@ export class ProductController {
}
}
@ApiOkResponse({
type: ProductRes,
})
@ApiOkResponse({ type: ProductRes })
@Put('updateNameCn/:id/:nameCn')
async updateProductNameCn(
@Param('id') id: number,
@Param('nameCn') nameCn: string
) {
async updateProductNameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
try {
const data = this.productService.updateProductNameCn(id, nameCn);
return successResponse(data);
@ -138,9 +105,7 @@ export class ProductController {
}
}
@ApiOkResponse({
type: BooleanRes,
})
@ApiOkResponse({ type: BooleanRes })
@Del('/:id')
async deleteProduct(@Param('id') id: number) {
try {
@ -151,14 +116,55 @@ export class ProductController {
}
}
@ApiOkResponse({
type: ProductBrandListRes,
})
@Get('/brands')
async getBrands(@Query() query: QueryBrandDTO) {
const { current = 1, pageSize = 10, name } = query;
// 中文注释:获取产品的库存组成
@ApiOkResponse()
@Get('/:id/components')
async getProductComponents(@Param('id') id: number) {
try {
let data = await this.productService.getBrandList(
const data = await this.productService.getProductComponents(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 中文注释:设置产品的库存组成(覆盖式)
@ApiOkResponse()
@Post('/:id/components')
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
try {
const data = await this.productService.setProductComponents(id, body?.items || []);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 中文注释:根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
@ApiOkResponse()
@Post('/:id/components/auto')
async autoBindComponents(@Param('id') id: number) {
try {
const data = await this.productService.autoBindComponentsBySku(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 通用属性接口:分页列表
@ApiOkResponse()
@Get('/attribute')
async getAttributeList(
@Query('dictName') dictName: string,
@Query('current') current = 1,
@Query('pageSize') pageSize = 10,
@Query('name') name?: string
) {
try {
const data = await this.productService.getAttributeList(
dictName,
{ current, pageSize },
name
);
@ -168,95 +174,142 @@ export class ProductController {
}
}
// 通用属性接口:全部列表
@ApiOkResponse()
@Get('/brandAll')
async getBrandAll() {
@Get('/attributeAll')
async getAttributeAll(@Query('dictName') dictName: string) {
try {
let data = await this.productService.getBrandAll();
const data = await this.productService.getAttributeAll(dictName);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductBrandRes,
})
@Post('/brand')
async createBrand(@Body() brandData: CreateBrandDTO) {
try {
const hasBrand = await this.productService.hasAttribute(
'brand',
brandData.name
);
if (hasBrand) {
return errorResponse('品牌已存在');
}
let data = await this.productService.createBrand(brandData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductBrandRes,
})
@Put('/brand/:id')
async updateBrand(
@Param('id') id: number,
@Body() brandData: UpdateBrandDTO
// 通用属性接口:创建
@ApiOkResponse()
@Post('/attribute')
async createAttribute(
@Query('dictName') dictName: string,
@Body() body: { title: string; name: string }
) {
try {
const hasBrand = await this.productService.hasAttribute(
'brand',
brandData.name,
const hasItem = await this.productService.hasAttribute(
dictName,
body.name
);
if (hasItem) return errorResponse('字典项已存在');
const data = await this.productService.createAttribute(dictName, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 通用属性接口:更新
@ApiOkResponse()
@Put('/attribute/:id')
async updateAttribute(
@Param('id') id: number,
@Query('dictName') dictName: string,
@Body() body: { title?: string; name?: string }
) {
try {
if (body?.name) {
const hasItem = await this.productService.hasAttribute(
dictName,
body.name,
id
);
if (hasBrand) {
return errorResponse('品牌已存在');
if (hasItem) return errorResponse('字典项已存在');
}
const data = this.productService.updateBrand(id, brandData);
const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Del('/brand/:id')
async deleteBrand(@Param('id') id: number) {
// 通用属性接口:删除
@ApiOkResponse({ type: BooleanRes })
@Del('/attribute/:id')
async deleteAttribute(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInAttribute(id);
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
const data = await this.productService.deleteBrand(id);
return successResponse(data);
await this.productService.deleteAttribute(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@Post('/batchSetSku')
@ApiOkResponse({
description: '批量设置 sku 的响应结果',
type: BooleanRes,
})
async batchSetSku(@Body() body: BatchSetSkuDTO) {
// 兼容旧接口:品牌
@ApiOkResponse()
@Get('/brandAll')
async compatBrandAll() {
try {
const result = await this.productService.batchSetSku(body.skus);
return successResponse(result, '批量设置 sku 成功');
const data = await this.productService.getAttributeAll('brand'); // 中文注释:返回所有品牌字典项
return successResponse(data);
} catch (error) {
return errorResponse(error.message, 400);
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/flavorsAll')
async getFlavorsAll() {
@Get('/brands')
async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
let data = await this.productService.getFlavorsAll();
const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 中文注释:分页品牌列表
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/brand')
async compatCreateBrand(@Body() body: { title: string; name: string }) {
try {
const has = await this.productService.hasAttribute('brand', body.name); // 中文注释:唯一性校验
if (has) return errorResponse('品牌已存在');
const data = await this.productService.createAttribute('brand', body); // 中文注释:创建品牌字典项
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/brand/:id')
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
try {
if (body?.name) {
const has = await this.productService.hasAttribute('brand', body.name, id); // 中文注释:唯一性校验(排除自身)
if (has) return errorResponse('品牌已存在');
}
const data = await this.productService.updateAttribute(id, body); // 中文注释:更新品牌字典项
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/brand/:id')
async compatDeleteBrand(@Param('id') id: number) {
try {
await this.productService.deleteAttribute(id); // 中文注释:删除品牌字典项
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 兼容旧接口:口味
@ApiOkResponse()
@Get('/flavorsAll')
async compatFlavorsAll() {
try {
const data = await this.productService.getAttributeAll('flavor');
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -265,13 +318,9 @@ export class ProductController {
@ApiOkResponse()
@Get('/flavors')
async getFlavors(@Query() query: QueryFlavorsDTO) {
const { current = 1, pageSize = 10, name } = query;
async compatFlavors(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
let data = await this.productService.getFlavorsList(
{ current, pageSize },
name
);
const data = await this.productService.getAttributeList('flavor', { current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -280,13 +329,11 @@ export class ProductController {
@ApiOkResponse()
@Post('/flavors')
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
async compatCreateFlavors(@Body() body: { title: string; name: string }) {
try {
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name);
if (hasFlavors) {
return errorResponse('口味已存在');
}
let data = await this.productService.createFlavors(flavorsData);
const has = await this.productService.hasAttribute('flavor', body.name);
if (has) return errorResponse('口味已存在');
const data = await this.productService.createAttribute('flavor', body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -295,42 +342,36 @@ export class ProductController {
@ApiOkResponse()
@Put('/flavors/:id')
async updateFlavors(
@Param('id') id: number,
@Body() flavorsData: UpdateFlavorsDTO
) {
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
try {
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name, id);
if (hasFlavors) {
return errorResponse('口味已存在');
if (body?.name) {
const has = await this.productService.hasAttribute('flavor', body.name, id);
if (has) return errorResponse('口味已存在');
}
const data = this.productService.updateFlavors(id, flavorsData);
const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@ApiOkResponse({ type: BooleanRes })
@Del('/flavors/:id')
async deleteFlavors(@Param('id') id: number) {
async compatDeleteFlavors(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInAttribute(id);
if (hasProducts) throw new Error('该口味下有商品,无法删除');
const data = await this.productService.deleteFlavors(id);
return successResponse(data);
await this.productService.deleteAttribute(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 兼容旧接口:规格
@ApiOkResponse()
@Get('/strengthAll')
async getStrengthAll() {
async compatStrengthAll() {
try {
let data = await this.productService.getStrengthAll();
const data = await this.productService.getAttributeAll('strength');
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -339,13 +380,9 @@ export class ProductController {
@ApiOkResponse()
@Get('/strength')
async getStrength(@Query() query: QueryStrengthDTO) {
const { current = 1, pageSize = 10, name } = query;
async compatStrength(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
let data = await this.productService.getStrengthList(
{ current, pageSize },
name
);
const data = await this.productService.getAttributeList('strength', { current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -354,16 +391,11 @@ export class ProductController {
@ApiOkResponse()
@Post('/strength')
async createStrength(@Body() strengthData: CreateStrengthDTO) {
async compatCreateStrength(@Body() body: { title: string; name: string }) {
try {
const hasStrength = await this.productService.hasAttribute(
'strength',
strengthData.name
);
if (hasStrength) {
return errorResponse('规格已存在');
}
let data = await this.productService.createStrength(strengthData);
const has = await this.productService.hasAttribute('strength', body.name);
if (has) return errorResponse('规格已存在');
const data = await this.productService.createAttribute('strength', body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -372,38 +404,89 @@ export class ProductController {
@ApiOkResponse()
@Put('/strength/:id')
async updateStrength(
@Param('id') id: number,
@Body() strengthData: UpdateStrengthDTO
) {
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
try {
const hasStrength = await this.productService.hasAttribute(
'strength',
strengthData.name,
id
);
if (hasStrength) {
return errorResponse('规格已存在');
if (body?.name) {
const has = await this.productService.hasAttribute('strength', body.name, id);
if (has) return errorResponse('规格已存在');
}
const data = this.productService.updateStrength(id, strengthData);
const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@ApiOkResponse({ type: BooleanRes })
@Del('/strength/:id')
async deleteStrength(@Param('id') id: number) {
async compatDeleteStrength(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInAttribute(id);
if (hasProducts) throw new Error('该规格下有商品,无法删除');
const data = await this.productService.deleteStrength(id);
await this.productService.deleteAttribute(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 兼容旧接口:尺寸
@ApiOkResponse()
@Get('/sizeAll')
async compatSizeAll() {
try {
const data = await this.productService.getAttributeAll('size');
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/size')
async compatSize(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
const data = await this.productService.getAttributeList('size', { current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/size')
async compatCreateSize(@Body() body: { title: string; name: string }) {
try {
const has = await this.productService.hasAttribute('size', body.name);
if (has) return errorResponse('尺寸已存在');
const data = await this.productService.createAttribute('size', body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/size/:id')
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
try {
if (body?.name) {
const has = await this.productService.hasAttribute('size', body.name, id);
if (has) return errorResponse('尺寸已存在');
}
const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/size/:id')
async compatDeleteSize(@Param('id') id: number) {
try {
await this.productService.deleteAttribute(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
}

View File

@ -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 || '获取失败');
}

View File

@ -40,10 +40,11 @@ export class UserController {
}
@Post('/add')
async addUser(@Body() body: { username: string; password: string }) {
const { username, password } = body;
async addUser(@Body() body: { username: string; password: string; remark?: string }) {
const { username, password, remark } = body;
try {
await this.userService.addUser(username, password);
// 中文注释:新增用户(支持备注)
await this.userService.addUser(username, password, remark);
return successResponse(true);
} catch (error) {
console.log(error);
@ -52,21 +53,76 @@ export class UserController {
}
@Get('/list')
async listUsers(@Query() query: { current: number; pageSize: number }) {
const { current = 1, pageSize = 10 } = query;
return successResponse(await this.userService.listUsers(current, pageSize));
async listUsers(
@Query()
query: {
current: number;
pageSize: number;
remark?: string;
username?: string;
isActive?: string;
isSuper?: string;
isAdmin?: string;
}
) {
const { current = 1, pageSize = 10, remark, username, isActive, isSuper, isAdmin } = query;
// 中文注释:将字符串布尔转换为真实布尔
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
// 中文注释:列表移除密码字段
const { items, total } = await this.userService.listUsers(current, pageSize, {
remark,
username,
isActive: toBool(isActive),
isSuper: toBool(isSuper),
isAdmin: toBool(isAdmin),
});
const safeItems = (items || []).map((it: any) => {
const { password, ...rest } = it || {};
return rest;
});
return successResponse({ items: safeItems, total, current, pageSize });
}
@Post('/toggleActive')
async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
return this.userService.toggleUserActive(body.userId, body.isActive);
try {
// 中文注释:调用服务层更新启用状态
const data = await this.userService.toggleUserActive(body.userId, body.isActive);
// 中文注释:移除密码字段,保证安全
const { password, ...safe } = data as any;
return successResponse(safe);
} catch (error) {
return errorResponse(error?.message || '操作失败');
}
}
// 中文注释:更新用户(支持用户名/密码/权限/角色更新)
@Post('/update/:id')
async updateUser(
@Body() body: { username?: string; password?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
@Query('id') id?: number
) {
try {
// 条件判断:优先从路径参数获取 ID兼容生成的 API 文件为 POST /user/update/:id
const userId = Number((this.ctx?.params?.id ?? id));
if (!userId) throw new Error('缺少用户ID');
const data = await this.userService.updateUser(userId, body);
// 中文注释:移除密码字段,保证安全
const { password, ...safe } = data as any;
return successResponse(safe);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse()
@Get()
async getUser(@User() user) {
try {
return successResponse(await this.userService.getUser(user.id));
// 中文注释:详情移除密码字段
const data = await this.userService.getUser(user.id);
const { password, ...safe } = (data as any) || {};
return successResponse(safe);
} catch (error) {
return errorResponse('获取失败');
}

View File

@ -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'],
};

View File

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

View File

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

View File

@ -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;
}
// 查询区域的数据传输对象

View File

@ -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,11 @@ export class UpdateDictItemDTO {
@Rule(RuleType.string())
title?: string; // 字典项标题 (可选)
@Rule(RuleType.string().allow(null))
titleCN?: string; // 字典项中文标题 (可选)
@Rule(RuleType.string().allow(null))
value?: string; // 字典项值 (可选)
}

View File

@ -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,50 +21,52 @@ 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: '规格', type: DictItemDTO })
@Rule(
RuleType.object().keys({
title: RuleType.string().required(),
name: RuleType.string(),
})
)
strength: DictItemDTO;
@ApiProperty({ description: '口味', type: DictItemDTO })
@Rule(
RuleType.object().keys({
title: RuleType.string().required(),
name: RuleType.string(),
})
)
flavor: DictItemDTO;
@ApiProperty()
@Rule(RuleType.string())
humidity: string;
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
@ApiProperty({ description: '属性列表', type: 'array' })
@Rule(RuleType.array().required())
attributes: AttributeInputDTO[];
// 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number())
price?: number;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
@Rule(RuleType.number())
promotionPrice?: number;
}
/**
* DTO
*/
export class UpdateProductDTO extends CreateProductDTO {
export class UpdateProductDTO {
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
@Rule(RuleType.string())
name: string;
name?: string;
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string())
description?: string;
@ApiProperty({ description: '产品 SKU', required: false })
@Rule(RuleType.string())
sku?: string;
// 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number())
price?: number;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
@Rule(RuleType.number())
promotionPrice?: number;
// 属性更新(中文注释:可选,支持增量替换指定字典的属性项)
@ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array())
attributes?: AttributeInputDTO[];
}
/**
@ -98,6 +90,25 @@ export class QueryProductDTO {
brandId: number;
}
// 属性输入项(中文注释:用于在创建/更新产品时传递字典项信息)
export class AttributeInputDTO {
@ApiProperty({ description: '字典名称', example: 'brand', required: false})
@Rule(RuleType.string())
dictName?: string;
@ApiProperty({ description: '字典项 ID', required: false })
@Rule(RuleType.number())
id?: number;
@ApiProperty({ description: '字典项显示名称', required: false })
@Rule(RuleType.string())
title?: string;
@ApiProperty({ description: '字典项唯一标识', required: false })
@Rule(RuleType.string())
name?: string;
}
/**
* DTO
*/
@ -213,6 +224,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;
@ -225,3 +271,21 @@ export class BatchSetSkuDTO {
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
skus: SkuItemDTO[];
}
// 中文注释:产品库存组成项输入
export class ProductComponentItemDTO {
@ApiProperty({ description: '库存记录ID' })
@Rule(RuleType.number().required())
stockId: number;
@ApiProperty({ description: '组成数量', example: 1 })
@Rule(RuleType.number().min(1).default(1))
quantity: number;
}
// 中文注释:设置产品库存组成输入
export class SetProductComponentsDTO {
@ApiProperty({ description: '组成项列表', type: [ProductComponentItemDTO] })
@Rule(RuleType.array().items(RuleType.object()))
items: ProductComponentItemDTO[];
}

View File

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

View File

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

View File

@ -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: '创建时间',

View File

@ -25,7 +25,9 @@ export class DictItem {
// 字典项名称
@Column({ comment: '字典项显示名称' })
title: string;
// 目前没有单独做国际化, 所以这里先添加 titleCN 用来标注
@Column({ comment: '字典项中文名称', nullable: true })
titleCN: string;
// 唯一标识
@Column({ unique: true, comment: '字典唯一标识名称' })
name: string;

View File

@ -6,9 +6,11 @@ import {
Entity,
ManyToMany,
JoinTable,
OneToMany,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { DictItem } from './dict_item.entity';
import { ProductStockComponent } from './product_stock_component.entity';
@Entity()
export class Product {
@ -34,6 +36,7 @@ export class Product {
@Column({ default: '' })
nameCn: string;
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Column({ nullable: true })
description?: string;
@ -46,18 +49,30 @@ export class Product {
@ApiProperty({ description: '价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
price: number;
// 类型 主要用来区分混装和单品 单品死
@ApiProperty({ description: '类型' })
@Column()
type: string;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number;
@ApiProperty({ description: '库存', example: 100 })
@Column({ default: 0 })
stock: number;
@ManyToMany(() => DictItem, {
cascade: true,
})
@JoinTable()
attributes: DictItem[];
// 中文注释:产品的库存组成,一对多关系(使用独立表)
@ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
components: ProductStockComponent[];
// 来源
@ApiProperty({ description: '来源', example: '1' })
@Column({ default: 0 })

View File

@ -0,0 +1,40 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { Product } from './product.entity';
import { Stock } from './stock.entity';
@Entity('product_stock_component')
export class ProductStockComponent {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
productId: number;
@ApiProperty({ type: Number })
@Column()
stockId: number;
@ApiProperty({ type: Number, description: '组成数量' })
@Column({ type: 'int', default: 1 })
quantity: number;
// 中文注释:多对一,组件隶属于一个产品
@ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
product: Product;
// 中文注释:多对一,组件引用一个库存记录
@ManyToOne(() => Stock, { eager: true, onDelete: 'CASCADE' })
stock: Stock;
@ApiProperty({ description: '创建时间' })
@CreateDateColumn()
createdAt: Date;
@ApiProperty({ description: '更新时间' })
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -28,4 +28,8 @@ export class User {
@Column({ default: true })
isActive: boolean; // 用户是否启用
// 中文注释:备注字段(可选)
@Column({ nullable: true })
remark?: string;
}

View File

@ -10,7 +10,7 @@ export interface WpSite {
wpApiUrl: string;
consumerKey: string;
consumerSecret: string;
siteName: string;
name: string;
email: string;
emailPswd: string;
}

View File

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

View File

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

View File

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

View File

@ -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,9 @@ 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';
import { Stock } from '../entity/stock.entity';
import { ProductStockComponent } from '../entity/product_stock_component.entity';
@Provide()
export class ProductService {
@ -35,6 +41,9 @@ export class ProductService {
@Inject()
templateService: TemplateService;
@Inject()
stockService: StockService;
@InjectEntityModel(Product)
productModel: Repository<Product>;
@ -50,6 +59,12 @@ export class ProductService {
@InjectEntityModel(Variation)
variationModel: Repository<Variation>;
@InjectEntityModel(Stock)
stockModel: Repository<Stock>;
@InjectEntityModel(ProductStockComponent)
productStockComponentModel: Repository<ProductStockComponent>;
// async findProductsByName(name: string): Promise<Product[]> {
// const where: any = {};
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
@ -157,34 +172,14 @@ export class ProductService {
const [items, total] = await qb.getManyAndCount();
// 格式化返回的数据
const formattedItems = items.map(product => {
const getAttributeTitle = (dictName: string) =>
product.attributes.find(a => a.dict.name === dictName)?.title || null;
return {
id: product.id,
name: product.name,
nameCn: product.nameCn,
description: product.description,
humidity: getAttributeTitle('humidity'),
sku: product.sku,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
brandName: getAttributeTitle('brand'),
flavorsName: getAttributeTitle('flavor'),
strengthName: getAttributeTitle('strength'),
};
});
return {
items: formattedItems,
items,
total,
...pagination,
};
}
async getOrCreateDictItem(
async getOrCreateAttribute(
dictName: string,
itemTitle: string,
itemName?: string
@ -213,31 +208,34 @@ export class ProductService {
}
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const { name, description, brand, flavor, strength, humidity, sku } =
createProductDTO;
const { name, description, attributes, 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 humidityItem = await this.getOrCreateDictItem('humidity', humidity);
// 条件判断(中文注释:校验属性输入)
if (!Array.isArray(attributes) || attributes.length === 0) {
throw new Error('属性列表不能为空');
}
// 检查具有完全相同属性组合的产品是否已存在
const attributesToMatch = [brandItem, flavorItem, strengthItem, humidityItem];
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
const resolvedAttributes: DictItem[] = [];
for (const attr of attributes) {
let item: DictItem | null = null;
if (attr.id) {
// 中文注释:如果传入了 id直接查找字典项并使用不强制要求 dictName
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
} else {
// 中文注释:当未提供 id 时,需要 dictName 与 title/name 信息创建或获取字典项
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
const titleOrName = attr.title || attr.name;
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
}
resolvedAttributes.push(item);
}
// 检查完全相同属性组合是否已存在(中文注释:避免重复)
const qb = this.productModel.createQueryBuilder('product');
attributesToMatch.forEach((attr, index) => {
resolvedAttributes.forEach((attr, index) => {
qb.innerJoin(
'product.attributes',
`attr${index}`,
@ -245,30 +243,40 @@ export class ProductService {
{ [`attrId${index}`]: attr.id }
);
});
const isExit = await qb.getOne();
const isExist = await qb.getOne();
if (isExist) throw new Error('产品已存在');
if (isExit) throw new Error('产品已存在');
// 创建新产品实例
// 创建新产品实例(中文注释:绑定属性与基础字段)
const product = new Product();
product.name = name;
product.description = description;
product.attributes = attributesToMatch;
product.attributes = resolvedAttributes;
// 如果用户提供了 sku则直接使用否则通过模板引擎生成
// 生成或设置 SKU中文注释基于属性字典项的 name 生成)
if (sku) {
product.sku = sku;
} else {
// 生成 SKU
const attributeMap: Record<string, string> = {};
for (const a of resolvedAttributes) {
if (a?.dict?.name && a?.name) attributeMap[a.dict.name] = a.name;
}
product.sku = await this.templateService.render('product_sku', {
brand: brandItem.name,
flavor: flavorItem.name,
strength: strengthItem.name,
humidity: humidityItem.name,
brand: attributeMap['brand'] || '',
flavor: attributeMap['flavor'] || '',
strength: attributeMap['strength'] || '',
humidity: attributeMap['humidity'] || '',
});
}
// 保存产品
// 价格与促销价(中文注释:可选字段)
if (price !== undefined) {
product.price = Number(price);
}
const promotionPrice = (createProductDTO as any)?.promotionPrice;
if (promotionPrice !== undefined) {
product.promotionPrice = Number(promotionPrice);
}
return await this.productModel.save(product);
}
@ -276,15 +284,133 @@ export class ProductService {
id: number,
updateProductDTO: UpdateProductDTO
): Promise<Product> {
// 确认产品是否存在
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;
}
}
// 处理属性更新(中文注释:若传入 attributes 则按字典名称替换对应项)
if (Array.isArray(updateProductDTO.attributes) && updateProductDTO.attributes.length > 0) {
const nextAttributes: DictItem[] = [...(product.attributes || [])];
const replaceAttr = (dictName: string, item: DictItem) => {
const idx = nextAttributes.findIndex(a => a.dict?.name === dictName);
if (idx >= 0) nextAttributes[idx] = item; else nextAttributes.push(item);
};
for (const attr of updateProductDTO.attributes) {
let item: DictItem | null = null;
if (attr.id) {
// 中文注释:当提供 id 时直接查询字典项,不强制要求 dictName
item = await this.dictItemModel.findOne({ where: { id: attr.id }, relations: ['dict'] });
if (!item) throw new Error(`字典项 ID ${attr.id} 不存在`);
} else {
// 中文注释:未提供 id 则需要 dictName 与 title/name 信息
if (!attr?.dictName) throw new Error('属性项缺少字典名称');
const titleOrName = attr.title || attr.name;
if (!titleOrName) throw new Error('新建字典项需要提供 title 或 name');
item = await this.getOrCreateAttribute(attr.dictName, titleOrName, attr.name);
}
// 中文注释:以传入的 dictName 或查询到的 item.dict.name 作为替换键
const dictKey = attr.dictName || item?.dict?.name;
if (!dictKey) throw new Error('无法确定字典名称用于替换属性');
replaceAttr(dictKey, item);
}
product.attributes = nextAttributes;
}
// 保存更新后的产品
const saved = await this.productModel.save(product);
return saved;
}
// 中文注释:获取产品的库存组成列表(表关联版本)
async getProductComponents(productId: number): Promise<ProductStockComponent[]> {
// 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
return await this.productStockComponentModel.find({ where: { productId } });
}
// 中文注释:设置产品的库存组成(覆盖式,表关联版本)
async setProductComponents(
productId: number,
items: { stockId: number; quantity: number }[]
): Promise<ProductStockComponent[]> {
// 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
const validItems = (items || [])
.filter(i => i && i.stockId && i.quantity && i.quantity > 0)
.map(i => ({ stockId: Number(i.stockId), quantity: Number(i.quantity) }));
// 删除旧的组成
await this.productStockComponentModel.delete({ productId });
// 插入新的组成
const created: ProductStockComponent[] = [];
for (const i of validItems) {
const stock = await this.stockModel.findOne({ where: { id: i.stockId } });
if (!stock) throw new Error(`库存 ID ${i.stockId} 不存在`);
const comp = new ProductStockComponent();
comp.productId = productId;
comp.stockId = i.stockId;
comp.quantity = i.quantity;
comp.stock = stock;
created.push(await this.productStockComponentModel.save(comp));
}
return created;
}
// 中文注释:根据 SKU 自动绑定产品的库存组成(匹配所有相同 SKU 的库存,默认数量 1
async autoBindComponentsBySku(productId: number): Promise<ProductStockComponent[]> {
// 条件判断:确保产品存在
const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
const stocks = await this.stockModel.find({ where: { productSku: product.sku } });
if (stocks.length === 0) return [];
for (const stock of stocks) {
// 条件判断:若已存在相同 stockId 的组成则跳过
const exist = await this.productStockComponentModel.findOne({ where: { productId, stockId: stock.id } });
if (exist) continue;
const comp = new ProductStockComponent();
comp.productId = productId;
comp.stockId = stock.id;
comp.quantity = 1; // 默认数量 1
comp.stock = stock;
await this.productStockComponentModel.save(comp);
}
return await this.getProductComponents(productId);
}
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
@ -523,6 +649,81 @@ export class ProductService {
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> {
const strengthDict = await this.dictModel.findOne({
@ -584,6 +785,75 @@ export class ProductService {
return await this.dictItemModel.save(strength);
}
// 通用属性:分页获取指定字典的字典项
async getAttributeList(
dictName: string,
pagination: PaginationParams,
name?: string
): Promise<BrandPaginatedResponse> {
const dict = await this.dictModel.findOne({ where: { name: dictName } });
if (!dict) return { items: [], total: 0, ...pagination } as any;
const where: any = { dict: { id: dict.id } };
if (name) where.title = Like(`%${name}%`);
const [items, total] = await this.dictItemModel.findAndCount({
where,
skip: (pagination.current - 1) * pagination.pageSize,
take: pagination.pageSize,
order: { sort: 'ASC', id: 'DESC' },
relations: ['dict'],
});
return { items, total, ...pagination } as any;
}
// 通用属性:获取指定字典的全部字典项
async getAttributeAll(dictName: string): Promise<DictItem[]> {
const dict = await this.dictModel.findOne({ where: { name: dictName } });
if (!dict) return [];
return this.dictItemModel.find({
where: { dict: { id: dict.id } },
order: { sort: 'ASC', id: 'DESC' },
relations: ['dict'],
});
}
// 通用属性:创建字典项
async createAttribute(
dictName: string,
payload: { title: string; name: string }
): Promise<DictItem> {
const dict = await this.dictModel.findOne({ where: { name: dictName } });
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
const exists = await this.dictItemModel.findOne({
where: { name: payload.name, dict: { id: dict.id } },
relations: ['dict'],
});
if (exists) throw new Error('字典项已存在');
const item = new DictItem();
item.title = payload.title;
item.name = payload.name;
item.dict = dict;
return await this.dictItemModel.save(item);
}
// 通用属性:更新字典项
async updateAttribute(
id: number,
payload: { title?: string; name?: string }
): Promise<DictItem> {
const item = await this.dictItemModel.findOne({ where: { id } });
if (!item) throw new Error('字典项不存在');
if (payload.title !== undefined) item.title = payload.title;
if (payload.name !== undefined) item.name = payload.name;
return await this.dictItemModel.save(item);
}
// 通用属性:删除字典项(若存在产品关联则禁止删除)
async deleteAttribute(id: number): Promise<void> {
const hasProducts = await this.hasProductsInAttribute(id);
if (hasProducts) throw new Error('当前字典项存在关联产品,无法删除');
await this.dictItemModel.delete({ id });
}
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
const strength = await this.dictItemModel.findOneBy({ id });
if (!strength) {

View File

@ -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<Site> = {
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) {

View File

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

View File

@ -1,7 +1,7 @@
// src/service/user.service.ts
import { Body, httpError, Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { Like, Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@midwayjs/jwt';
import { User } from '../entity/user.entity';
@ -81,7 +81,8 @@ export class UserService {
};
}
async addUser(username: string, password: string) {
// 中文注释:新增用户(支持可选备注)
async addUser(username: string, password: string, remark?: string) {
const existingUser = await this.userModel.findOne({
where: { username },
});
@ -92,14 +93,37 @@ export class UserService {
const user = this.userModel.create({
username,
password: hashedPassword,
// 中文注释:备注字段赋值(若提供)
...(remark ? { remark } : {}),
});
return this.userModel.save(user);
}
async listUsers(current: number, pageSize: number) {
// 中文注释:用户列表支持分页与备注模糊查询(以及可选的布尔过滤)
async listUsers(
current: number,
pageSize: number,
filters: {
remark?: string;
username?: string;
isActive?: boolean;
isSuper?: boolean;
isAdmin?: boolean;
} = {}
) {
// 条件判断:构造 where 条件
const where: Record<string, any> = {};
if (filters.username) where.username = Like(`%${filters.username}%`); // 中文注释:用户名精确匹配(如需模糊可改为 Like
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 中文注释:按启用状态过滤
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 中文注释:按超管过滤
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 中文注释:按管理员过滤
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 中文注释:备注模糊搜索
const [items, total] = await this.userModel.findAndCount({
where,
skip: (current - 1) * pageSize,
take: pageSize,
order: { id: 'DESC' },
});
return { items, total, current, pageSize };
}
@ -113,6 +137,48 @@ export class UserService {
return this.userModel.save(user);
}
// 中文注释:更新用户信息(支持用户名唯一校验与可选密码修改)
async updateUser(
userId: number,
payload: {
username?: string;
password?: string;
isSuper?: boolean;
isAdmin?: boolean;
permissions?: string[];
remark?: string;
}
) {
// 条件判断:查询用户是否存在
const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}
// 条件判断:若提供了新用户名且与原用户名不同,校验唯一性
if (payload.username && payload.username !== user.username) {
const exist = await this.userModel.findOne({ where: { username: payload.username } });
if (exist) throw new Error('用户名已存在');
user.username = payload.username;
}
// 条件判断:若提供密码则进行加密存储
if (payload.password) {
user.password = await bcrypt.hash(payload.password, 10);
}
// 条件判断:更新布尔与权限字段(若提供则覆盖)
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
if (Array.isArray(payload.permissions)) user.permissions = payload.permissions;
// 条件判断:更新备注(若提供则覆盖)
if (typeof payload.remark === 'string') user.remark = payload.remark;
// 保存更新
return await this.userModel.save(user);
}
async getUser(userId: number) {
return plainToInstance(
User,