feat: 添加分类管理和区域关联功能
实现分类实体、控制器和服务,支持分类与产品的关联 在站点和仓库点DTO中添加区域字段,支持区域关联查询 更新数据库配置和种子数据,优化产品属性管理
This commit is contained in:
parent
f20f4727f6
commit
0180360519
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const mysql = require("mysql2/promise");
|
||||
async function run() {
|
||||
try {
|
||||
const connection = await mysql.createConnection({
|
||||
socketPath: '/Users/zksu/Library/Application Support/Local/run/oLbUT7qMU/mysql/mysqld.sock',
|
||||
user: 'root',
|
||||
password: 'root',
|
||||
});
|
||||
console.log('Connected to database server.');
|
||||
await connection.query('CREATE DATABASE IF NOT EXISTS inventory');
|
||||
console.log('Database "inventory" created or already exists.');
|
||||
await connection.end();
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error:', e);
|
||||
}
|
||||
}
|
||||
run();
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlX2RiLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiY3JlYXRlX2RiLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQ0Esd0NBQXdDO0FBRXhDLEtBQUssVUFBVSxHQUFHO0lBQ2hCLElBQUksQ0FBQztRQUNILE1BQU0sVUFBVSxHQUFHLE1BQU0sS0FBSyxDQUFDLGdCQUFnQixDQUFDO1lBQzlDLFVBQVUsRUFBRSwrRUFBK0U7WUFDM0YsSUFBSSxFQUFFLE1BQU07WUFDWixRQUFRLEVBQUUsTUFBTTtTQUNqQixDQUFDLENBQUM7UUFFSCxPQUFPLENBQUMsR0FBRyxDQUFDLCtCQUErQixDQUFDLENBQUM7UUFFN0MsTUFBTSxVQUFVLENBQUMsS0FBSyxDQUFDLHlDQUF5QyxDQUFDLENBQUM7UUFDbEUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpREFBaUQsQ0FBQyxDQUFDO1FBRS9ELE1BQU0sVUFBVSxDQUFDLEdBQUcsRUFBRSxDQUFDO0lBQ3pCLENBQUM7SUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1FBQ1gsT0FBTyxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDN0IsQ0FBQztBQUNILENBQUM7QUFFRCxHQUFHLEVBQUUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIlxuaW1wb3J0ICogYXMgbXlzcWwgZnJvbSAnbXlzcWwyL3Byb21pc2UnO1xuXG5hc3luYyBmdW5jdGlvbiBydW4oKSB7XG4gIHRyeSB7XG4gICAgY29uc3QgY29ubmVjdGlvbiA9IGF3YWl0IG15c3FsLmNyZWF0ZUNvbm5lY3Rpb24oe1xuICAgICAgc29ja2V0UGF0aDogJy9Vc2Vycy96a3N1L0xpYnJhcnkvQXBwbGljYXRpb24gU3VwcG9ydC9Mb2NhbC9ydW4vb0xiVVQ3cU1VL215c3FsL215c3FsZC5zb2NrJyxcbiAgICAgIHVzZXI6ICdyb290JyxcbiAgICAgIHBhc3N3b3JkOiAncm9vdCcsXG4gICAgfSk7XG5cbiAgICBjb25zb2xlLmxvZygnQ29ubmVjdGVkIHRvIGRhdGFiYXNlIHNlcnZlci4nKTtcblxuICAgIGF3YWl0IGNvbm5lY3Rpb24ucXVlcnkoJ0NSRUFURSBEQVRBQkFTRSBJRiBOT1QgRVhJU1RTIGludmVudG9yeScpO1xuICAgIGNvbnNvbGUubG9nKCdEYXRhYmFzZSBcImludmVudG9yeVwiIGNyZWF0ZWQgb3IgYWxyZWFkeSBleGlzdHMuJyk7XG4gICAgXG4gICAgYXdhaXQgY29ubmVjdGlvbi5lbmQoKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnNvbGUuZXJyb3IoJ0Vycm9yOicsIGUpO1xuICB9XG59XG5cbnJ1bigpO1xuIl19
|
||||
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const mysql = require("mysql2/promise");
|
||||
async function run() {
|
||||
try {
|
||||
const connection = await mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
port: 10014,
|
||||
user: 'root',
|
||||
password: 'root',
|
||||
database: 'inventory'
|
||||
});
|
||||
console.log('Connected to database.');
|
||||
const tables = ['product', 'category_attribute', 'category'];
|
||||
for (const table of tables) {
|
||||
console.log(`\nConstraints for table: ${table}`);
|
||||
const [rows] = await connection.execute(`
|
||||
SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE
|
||||
FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE TABLE_SCHEMA = 'inventory' AND TABLE_NAME = '${table}'
|
||||
`);
|
||||
console.table(rows);
|
||||
}
|
||||
await connection.end();
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Error:', e);
|
||||
}
|
||||
}
|
||||
run();
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlhZ25vc2VfZGIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJkaWFnbm9zZV9kYi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUNBLHdDQUF3QztBQUV4QyxLQUFLLFVBQVUsR0FBRztJQUNoQixJQUFJLENBQUM7UUFDSCxNQUFNLFVBQVUsR0FBRyxNQUFNLEtBQUssQ0FBQyxnQkFBZ0IsQ0FBQztZQUM5QyxJQUFJLEVBQUUsV0FBVztZQUNqQixJQUFJLEVBQUUsS0FBSztZQUNYLElBQUksRUFBRSxNQUFNO1lBQ1osUUFBUSxFQUFFLE1BQU07WUFDaEIsUUFBUSxFQUFFLFdBQVc7U0FDdEIsQ0FBQyxDQUFDO1FBRUgsT0FBTyxDQUFDLEdBQUcsQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO1FBRXRDLE1BQU0sTUFBTSxHQUFHLENBQUMsU0FBUyxFQUFFLG9CQUFvQixFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBRTdELEtBQUssTUFBTSxLQUFLLElBQUksTUFBTSxFQUFFLENBQUM7WUFDM0IsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0QkFBNEIsS0FBSyxFQUFFLENBQUMsQ0FBQztZQUNqRCxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsTUFBTSxVQUFVLENBQUMsT0FBTyxDQUFDOzs7NkRBR2UsS0FBSztPQUMzRCxDQUFDLENBQUM7WUFDSCxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3RCLENBQUM7UUFFRCxNQUFNLFVBQVUsQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUN6QixDQUFDO0lBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztRQUNYLE9BQU8sQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQzdCLENBQUM7QUFDSCxDQUFDO0FBRUQsR0FBRyxFQUFFLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJcbmltcG9ydCAqIGFzIG15c3FsIGZyb20gJ215c3FsMi9wcm9taXNlJztcblxuYXN5bmMgZnVuY3Rpb24gcnVuKCkge1xuICB0cnkge1xuICAgIGNvbnN0IGNvbm5lY3Rpb24gPSBhd2FpdCBteXNxbC5jcmVhdGVDb25uZWN0aW9uKHtcbiAgICAgIGhvc3Q6ICcxMjcuMC4wLjEnLFxuICAgICAgcG9ydDogMTAwMTQsXG4gICAgICB1c2VyOiAncm9vdCcsXG4gICAgICBwYXNzd29yZDogJ3Jvb3QnLFxuICAgICAgZGF0YWJhc2U6ICdpbnZlbnRvcnknXG4gICAgfSk7XG5cbiAgICBjb25zb2xlLmxvZygnQ29ubmVjdGVkIHRvIGRhdGFiYXNlLicpO1xuXG4gICAgY29uc3QgdGFibGVzID0gWydwcm9kdWN0JywgJ2NhdGVnb3J5X2F0dHJpYnV0ZScsICdjYXRlZ29yeSddO1xuXG4gICAgZm9yIChjb25zdCB0YWJsZSBvZiB0YWJsZXMpIHtcbiAgICAgIGNvbnNvbGUubG9nKGBcXG5Db25zdHJhaW50cyBmb3IgdGFibGU6ICR7dGFibGV9YCk7XG4gICAgICBjb25zdCBbcm93c10gPSBhd2FpdCBjb25uZWN0aW9uLmV4ZWN1dGUoYFxuICAgICAgICBTRUxFQ1QgQ09OU1RSQUlOVF9OQU1FLCBDT05TVFJBSU5UX1RZUEUgXG4gICAgICAgIEZST00gaW5mb3JtYXRpb25fc2NoZW1hLlRBQkxFX0NPTlNUUkFJTlRTIFxuICAgICAgICBXSEVSRSBUQUJMRV9TQ0hFTUEgPSAnaW52ZW50b3J5JyBBTkQgVEFCTEVfTkFNRSA9ICcke3RhYmxlfSdcbiAgICAgIGApO1xuICAgICAgY29uc29sZS50YWJsZShyb3dzKTtcbiAgICB9XG4gICAgXG4gICAgYXdhaXQgY29ubmVjdGlvbi5lbmQoKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnNvbGUuZXJyb3IoJ0Vycm9yOicsIGUpO1xuICB9XG59XG5cbnJ1bigpO1xuIl19
|
||||
|
|
@ -36,7 +36,11 @@ 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 { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||
import { Category } from '../entity/category.entity';
|
||||
import DictSeeder from '../db/seeds/dict.seeder';
|
||||
import CategorySeeder from '../db/seeds/category.seeder';
|
||||
import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder';
|
||||
|
||||
export default {
|
||||
// use for cookie sign key, should change to your own and keep security
|
||||
|
|
@ -81,16 +85,18 @@ export default {
|
|||
DictItem,
|
||||
Template,
|
||||
Area,
|
||||
CategoryAttribute,
|
||||
Category,
|
||||
],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
seeders: [DictSeeder],
|
||||
seeders: [DictSeeder, CategorySeeder, CategoryAttributeSeeder],
|
||||
},
|
||||
dataSource: {
|
||||
default: {
|
||||
type: 'mysql',
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
port: 10014,
|
||||
username: 'root',
|
||||
password: 'root',
|
||||
database: 'inventory',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { Controller, Get, Post, Put, Del, Body, Query, Inject, Param } from '@midwayjs/core';
|
||||
import { CategoryService } from '../service/category.service';
|
||||
import { successResponse, errorResponse } from '../utils/response.util';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
|
||||
@Controller('/category')
|
||||
export class CategoryController {
|
||||
@Inject()
|
||||
categoryService: CategoryService;
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/all')
|
||||
async getAll() {
|
||||
try {
|
||||
const data = await this.categoryService.getAll();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/')
|
||||
async getList(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
|
||||
try {
|
||||
const data = await this.categoryService.getList({ current, pageSize }, name);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('/')
|
||||
async create(@Body() body: any) {
|
||||
try {
|
||||
const data = await this.categoryService.create(body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Put('/:id')
|
||||
async update(@Param('id') id: number, @Body() body: any) {
|
||||
try {
|
||||
const data = await this.categoryService.update(id, body);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Del('/:id')
|
||||
async delete(@Param('id') id: number) {
|
||||
try {
|
||||
await this.categoryService.delete(id);
|
||||
return successResponse(null);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/attribute/:categoryId')
|
||||
async getCategoryAttributes(@Param('categoryId') categoryId: number) {
|
||||
try {
|
||||
const data = await this.categoryService.getCategoryAttributes(categoryId);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('/attribute')
|
||||
async createCategoryAttribute(@Body() body: { categoryId: number, attributeDictIds: number[] }) {
|
||||
try {
|
||||
const data = await this.categoryService.createCategoryAttribute(body.categoryId, body.attributeDictIds);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Del('/attribute/:id')
|
||||
async deleteCategoryAttribute(@Param('id') id: number) {
|
||||
try {
|
||||
await this.categoryService.deleteCategoryAttribute(id);
|
||||
return successResponse(null);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ import { Context } from '@midwayjs/koa';
|
|||
export class ProductController {
|
||||
@Inject()
|
||||
productService: ProductService;
|
||||
ProductRes;
|
||||
|
||||
@Inject()
|
||||
ctx: Context;
|
||||
|
|
@ -171,7 +170,7 @@ export class ProductController {
|
|||
@Post('/:id/components')
|
||||
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
|
||||
try {
|
||||
const data = await this.productService.setProductComponents(id, body?.items || []);
|
||||
const data = await this.productService.setProductComponents(id, body?.components || []);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -191,6 +190,20 @@ export class ProductController {
|
|||
}
|
||||
|
||||
|
||||
// 获取所有 WordPress 商品
|
||||
@ApiOkResponse({ description: '获取所有 WordPress 商品' })
|
||||
@Get('/wp-products')
|
||||
async getWpProducts() {
|
||||
try {
|
||||
const data = await this.productService.getWpProducts();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 通用属性接口:分页列表
|
||||
@ApiOkResponse()
|
||||
@Get('/attribute')
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { SeederOptions } from 'typeorm-extension';
|
|||
|
||||
const options: DataSourceOptions & SeederOptions = {
|
||||
type: 'mysql',
|
||||
host: 'localhost',
|
||||
port: 23306,
|
||||
// host: 'localhost',
|
||||
// port: 10014,
|
||||
socketPath: '/Users/zksu/Library/Application Support/Local/run/oLbUT7qMU/mysql/mysqld.sock',
|
||||
username: 'root',
|
||||
password: '12345678',
|
||||
password: 'root',
|
||||
database: 'inventory',
|
||||
synchronize: false,
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
entities: [__dirname + '/../entity/*.ts'],
|
||||
migrations: ['src/db/migrations/**/*.ts'],
|
||||
|
|
|
|||
|
|
@ -11,15 +11,20 @@ export default class AreaSeeder implements Seeder {
|
|||
const areaRepository = dataSource.getRepository(Area);
|
||||
|
||||
const areas = [
|
||||
{ name: 'Australia' },
|
||||
{ name: 'Canada' },
|
||||
{ name: 'United States' },
|
||||
{ name: 'Germany' },
|
||||
{ name: 'Poland' },
|
||||
{ name: 'Australia', code: 'AU' },
|
||||
{ name: 'Canada', code: 'CA' },
|
||||
{ name: 'United States', code: 'US' },
|
||||
{ name: 'Germany', code: 'DE' },
|
||||
{ name: 'Poland', code: 'PL' },
|
||||
];
|
||||
|
||||
for (const areaData of areas) {
|
||||
const existingArea = await areaRepository.findOne({ where: { name: areaData.name } });
|
||||
const existingArea = await areaRepository.findOne({
|
||||
where: [
|
||||
{ name: areaData.name },
|
||||
{ code: areaData.code }
|
||||
]
|
||||
});
|
||||
if (!existingArea) {
|
||||
const newArea = areaRepository.create(areaData);
|
||||
await areaRepository.save(newArea);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { Seeder } from 'typeorm-extension';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Category } from '../../entity/category.entity';
|
||||
|
||||
export default class CategorySeeder implements Seeder {
|
||||
public async run(
|
||||
dataSource: DataSource,
|
||||
): Promise<any> {
|
||||
const repository = dataSource.getRepository(Category);
|
||||
|
||||
const categories = [
|
||||
{
|
||||
name: 'nicotine-pouches',
|
||||
title: 'Nicotine Pouches',
|
||||
titleCN: '尼古丁袋',
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
name: 'vape',
|
||||
title: 'vape',
|
||||
titleCN: '电子烟',
|
||||
sort: 2
|
||||
},
|
||||
{
|
||||
name: 'pouches-can',
|
||||
title: 'Pouches Can',
|
||||
titleCN: ' nicotine 袋',
|
||||
sort: 3
|
||||
},
|
||||
];
|
||||
|
||||
for (const cat of categories) {
|
||||
const existing = await repository.findOne({ where: { name: cat.name } });
|
||||
if (!existing) {
|
||||
await repository.save(cat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { Seeder } from 'typeorm-extension';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Dict } from '../../entity/dict.entity';
|
||||
import { Category } from '../../entity/category.entity';
|
||||
import { CategoryAttribute } from '../../entity/category_attribute.entity';
|
||||
|
||||
export default class CategoryAttributeSeeder implements Seeder {
|
||||
public async run(
|
||||
dataSource: DataSource,
|
||||
): Promise<any> {
|
||||
const dictRepository = dataSource.getRepository(Dict);
|
||||
const categoryRepository = dataSource.getRepository(Category);
|
||||
const categoryAttributeRepository = dataSource.getRepository(CategoryAttribute);
|
||||
|
||||
// 1. 确保属性字典存在
|
||||
const attributeNames = ['brand', 'strength', 'flavor', 'size', 'humidity'];
|
||||
const attributeDicts: Dict[] = [];
|
||||
|
||||
for (const name of attributeNames) {
|
||||
let dict = await dictRepository.findOne({ where: { name } });
|
||||
if (!dict) {
|
||||
dict = new Dict();
|
||||
dict.name = name;
|
||||
dict.title = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
dict.deletable = false;
|
||||
dict = await dictRepository.save(dict);
|
||||
console.log(`Created Dict: ${name}`);
|
||||
}
|
||||
attributeDicts.push(dict);
|
||||
}
|
||||
|
||||
// 2. 获取 'nicotine-pouches' 分类 (由 CategorySeeder 创建)
|
||||
const nicotinePouchesCategory = await categoryRepository.findOne({
|
||||
where: {
|
||||
name: 'nicotine-pouches'
|
||||
}
|
||||
});
|
||||
|
||||
if (!nicotinePouchesCategory) {
|
||||
console.warn('Category "nicotine-pouches" not found. Skipping attribute linking. Please ensure CategorySeeder runs first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 绑定属性到 'nicotine-pouches' 分类
|
||||
for (const attrDict of attributeDicts) {
|
||||
const existing = await categoryAttributeRepository.findOne({
|
||||
where: {
|
||||
category: { id: nicotinePouchesCategory.id },
|
||||
attributeDict: { id: attrDict.id }
|
||||
}
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const link = new CategoryAttribute();
|
||||
link.category = nicotinePouchesCategory;
|
||||
link.attributeDict = attrDict;
|
||||
await categoryAttributeRepository.save(link);
|
||||
console.log(`Linked ${attrDict.name} to ${nicotinePouchesCategory.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,79 +22,77 @@ export default class DictSeeder implements Seeder {
|
|||
const dictItemRepository = dataSource.getRepository(DictItem);
|
||||
|
||||
const flavorsData = [
|
||||
{ title: 'Bellini', name: 'bellini' },
|
||||
{ title: 'Max Polarmint', name: 'max-polarmint' },
|
||||
{ title: 'Blueberry', name: 'blueberry' },
|
||||
{ title: 'Citrus', name: 'citrus' },
|
||||
{ title: 'Wintergreen', name: 'wintergreen' },
|
||||
{ title: 'COOL MINT', name: 'cool-mint' },
|
||||
{ title: 'JUICY PEACH', name: 'juicy-peach' },
|
||||
{ title: 'ORANGE', name: 'orange' },
|
||||
{ title: 'PEPPERMINT', name: 'peppermint' },
|
||||
{ title: 'SPEARMINT', name: 'spearmint' },
|
||||
{ title: 'STRAWBERRY', name: 'strawberry' },
|
||||
{ title: 'WATERMELON', name: 'watermelon' },
|
||||
{ title: 'COFFEE', name: 'coffee' },
|
||||
{ title: 'LEMONADE', name: 'lemonade' },
|
||||
{ title: 'apple mint', name: 'apple-mint' },
|
||||
{ title: 'PEACH', name: 'peach' },
|
||||
{ title: 'Mango', name: 'mango' },
|
||||
{ title: 'ICE WINTERGREEN', name: 'ice-wintergreen' },
|
||||
{ title: 'Pink Lemonade', name: 'pink-lemonade' },
|
||||
{ title: 'Blackcherry', name: 'blackcherry' },
|
||||
{ title: 'fresh mint', name: 'fresh-mint' },
|
||||
{ title: 'Strawberry Lychee', name: 'strawberry-lychee' },
|
||||
{ title: 'Passion Fruit', name: 'passion-fruit' },
|
||||
{ title: 'Banana lce', name: 'banana-lce' },
|
||||
{ title: 'Bubblegum', name: 'bubblegum' },
|
||||
{ title: 'Mango lce', name: 'mango-lce' },
|
||||
{ title: 'Grape lce', name: 'grape-lce' },
|
||||
{ title: 'apple', name: 'apple' },
|
||||
{ title: 'grape', name: 'grape' },
|
||||
{ title: 'cherry', name: 'cherry' },
|
||||
{ title: 'lemon', name: 'lemon' },
|
||||
{ title: 'razz', name: 'razz' },
|
||||
{ title: 'pineapple', name: 'pineapple' },
|
||||
{ title: 'berry', name: 'berry' },
|
||||
{ title: 'fruit', name: 'fruit' },
|
||||
{ title: 'mint', name: 'mint' },
|
||||
{ title: 'menthol', name: 'menthol' },
|
||||
{ name: 'bellini', title: 'Bellini', titleCn: '贝利尼' },
|
||||
{ name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷' },
|
||||
{ name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓' },
|
||||
{ name: 'citrus', title: 'Citrus', titleCn: '柑橘' },
|
||||
{ name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷' },
|
||||
{ name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷' },
|
||||
{ name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃' },
|
||||
{ name: 'orange', title: 'ORANGE', titleCn: '橙子' },
|
||||
{ name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷' },
|
||||
{ name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷' },
|
||||
{ name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓' },
|
||||
{ name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜' },
|
||||
{ name: 'coffee', title: 'COFFEE', titleCn: '咖啡' },
|
||||
{ name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水' },
|
||||
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷' },
|
||||
{ name: 'peach', title: 'PEACH', titleCn: '桃子' },
|
||||
{ name: 'mango', title: 'Mango', titleCn: '芒果' },
|
||||
{ name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷' },
|
||||
{ name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水' },
|
||||
{ name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃' },
|
||||
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷' },
|
||||
{ name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝' },
|
||||
{ name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果' },
|
||||
{ name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰' },
|
||||
{ name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖' },
|
||||
{ name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰' },
|
||||
{ name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰' },
|
||||
{ name: 'apple', title: 'apple', titleCn: '苹果' },
|
||||
{ name: 'grape', title: 'grape', titleCn: '葡萄' },
|
||||
{ name: 'cherry', title: 'cherry', titleCn: '樱桃' },
|
||||
{ name: 'lemon', title: 'lemon', titleCn: '柠檬' },
|
||||
{ name: 'razz', title: 'razz', titleCn: '覆盆子' },
|
||||
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝' },
|
||||
{ name: 'berry', title: 'berry', titleCn: '浆果' },
|
||||
{ name: 'fruit', title: 'fruit', titleCn: '水果' },
|
||||
{ name: 'mint', title: 'mint', titleCn: '薄荷' },
|
||||
{ name: 'menthol', title: 'menthol', titleCn: '薄荷醇' },
|
||||
];
|
||||
|
||||
const brandsData = [
|
||||
{ title: 'Yoone', name: 'yoone' },
|
||||
{ title: 'White Fox', name: 'white-fox' },
|
||||
{ title: 'ZYN', name: 'zyn' },
|
||||
{ title: 'Zonnic', name: 'zonnic' },
|
||||
{ title: 'Zolt', name: 'zolt' },
|
||||
{ title: 'Velo', name: 'velo' },
|
||||
{ title: 'Lucy', name: 'lucy' },
|
||||
{ title: 'EGP', name: 'egp' },
|
||||
{ title: 'Bridge', name: 'bridge' },
|
||||
{ title: 'ZEX', name: 'zex' },
|
||||
{ title: 'Sesh', name: 'sesh' },
|
||||
{ title: 'Pablo', name: 'pablo' },
|
||||
{ name: 'yoone', title: 'Yoone', titleCn: '' },
|
||||
{ name: 'white-fox', title: 'White Fox', titleCn: '' },
|
||||
{ name: 'zyn', title: 'ZYN', titleCn: '' },
|
||||
{ name: 'zonnic', title: 'Zonnic', titleCn: '' },
|
||||
{ name: 'zolt', title: 'Zolt', titleCn: '' },
|
||||
{ name: 'velo', title: 'Velo', titleCn: '' },
|
||||
{ name: 'lucy', title: 'Lucy', titleCn: '' },
|
||||
{ name: 'egp', title: 'EGP', titleCn: '' },
|
||||
{ name: 'bridge', title: 'Bridge', titleCn: '' },
|
||||
{ name: 'zex', title: 'ZEX', titleCn: '' },
|
||||
{ name: 'sesh', title: 'Sesh', titleCn: '' },
|
||||
{ name: 'pablo', title: 'Pablo', titleCn: '' },
|
||||
];
|
||||
|
||||
const strengthsData = [
|
||||
{ title: '3MG', name: '3mg' },
|
||||
{ title: '9MG', name: '9mg' },
|
||||
{ title: '2MG', name: '2mg' },
|
||||
{ title: '4MG', name: '4mg' },
|
||||
{ title: '12MG', name: '12mg' },
|
||||
{ title: '18MG', name: '18mg' },
|
||||
{ title: '6MG', name: '6mg' },
|
||||
{ title: '16.5MG', name: '16-5mg' },
|
||||
{ title: '6.5MG', name: '6-5mg' },
|
||||
{ title: '30MG', name: '30mg' },
|
||||
{ name: '2mg', title: '2MG', titleCn: '2毫克' },
|
||||
{ name: '4mg', title: '4MG', titleCn: '4毫克' },
|
||||
{ name: '3mg', title: '3MG', titleCn: '3毫克' },
|
||||
{ name: '6mg', title: '6MG', titleCn: '6毫克' },
|
||||
{ name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克' },
|
||||
{ name: '9mg', title: '9MG', titleCn: '9毫克' },
|
||||
{ name: '12mg', title: '12MG', titleCn: '12毫克' },
|
||||
{ name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克' },
|
||||
{ name: '18mg', title: '18MG', titleCn: '18毫克' },
|
||||
{ name: '30mg', title: '30MG', titleCn: '30毫克' },
|
||||
];
|
||||
|
||||
const nonFlavorTokensData = ['slim', 'pouches', 'pouch', 'mini', 'dry'].map(item => ({ title: item, name: item }));
|
||||
|
||||
// 初始化语言字典
|
||||
const locales = [
|
||||
{ name: 'zh-cn', title: '简体中文' },
|
||||
{ name: 'en-us', title: 'English' },
|
||||
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文' },
|
||||
{ name: 'en-us', title: 'English', titleCn: '英文' },
|
||||
];
|
||||
|
||||
for (const locale of locales) {
|
||||
|
|
@ -116,20 +114,19 @@ export default class DictSeeder implements Seeder {
|
|||
// 添加中文翻译
|
||||
let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
|
||||
if (!item) {
|
||||
await dictItemRepository.save({ name: t.name, title: t.zh, dict: zhDict });
|
||||
await dictItemRepository.save({ name: t.name, title: t.zh, titleCn: t.zh, dict: zhDict });
|
||||
}
|
||||
|
||||
// 添加英文翻译
|
||||
item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
|
||||
if (!item) {
|
||||
await dictItemRepository.save({ name: t.name, title: t.en, dict: enDict });
|
||||
await dictItemRepository.save({ name: t.name, title: t.en, titleCn: t.en, dict: enDict });
|
||||
}
|
||||
}
|
||||
|
||||
const brandDict = await this.createOrFindDict(dictRepository, { title: '品牌', name: 'brand' });
|
||||
const flavorDict = await this.createOrFindDict(dictRepository, { title: '口味', name: 'flavor' });
|
||||
const strengthDict = await this.createOrFindDict(dictRepository, { title: '强度', name: 'strength' });
|
||||
const nonFlavorTokensDict = await this.createOrFindDict(dictRepository, { title: '非口味关键词', name: 'non-flavor-tokens' });
|
||||
const brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌' });
|
||||
const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味' });
|
||||
const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度' });
|
||||
|
||||
// 遍历品牌数据
|
||||
await this.seedDictItems(dictItemRepository, brandDict, brandsData);
|
||||
|
|
@ -139,9 +136,6 @@ export default class DictSeeder implements Seeder {
|
|||
|
||||
// 遍历强度数据
|
||||
await this.seedDictItems(dictItemRepository, strengthDict, strengthsData);
|
||||
|
||||
// 遍历非口味关键词数据
|
||||
await this.seedDictItems(dictItemRepository, nonFlavorTokensDict, nonFlavorTokensData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -150,13 +144,13 @@ export default class DictSeeder implements Seeder {
|
|||
* @param dictInfo 字典信息
|
||||
* @returns Dict 实例
|
||||
*/
|
||||
private async createOrFindDict(repo: any, dictInfo: { title: string; name: string }): Promise<Dict> {
|
||||
private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string }): Promise<Dict> {
|
||||
// 格式化 name
|
||||
const formattedName = this.formatName(dictInfo.name);
|
||||
let dict = await repo.findOne({ where: { name: formattedName } });
|
||||
if (!dict) {
|
||||
// 如果字典不存在,则使用格式化后的 name 创建新字典
|
||||
dict = await repo.save({ title: dictInfo.title, name: formattedName });
|
||||
dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn });
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
|
@ -167,14 +161,14 @@ export default class DictSeeder implements Seeder {
|
|||
* @param dict 字典实例
|
||||
* @param items 字典项数组
|
||||
*/
|
||||
private async seedDictItems(repo: any, dict: Dict, items: { title: string; name: string }[]): Promise<void> {
|
||||
private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string }[]): Promise<void> {
|
||||
for (const item of items) {
|
||||
// 格式化 name
|
||||
const formattedName = this.formatName(item.name);
|
||||
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
|
||||
if (!existingItem) {
|
||||
// 如果字典项不存在,则使用格式化后的 name 创建新字典项
|
||||
await repo.save({ ...item, name: formattedName, dict });
|
||||
await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, dict });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,31 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Rule, RuleType } from '@midwayjs/validate';
|
||||
|
||||
/**
|
||||
* 属性输入DTO
|
||||
*/
|
||||
export class AttributeInputDTO {
|
||||
@ApiProperty({ description: '属性字典标识', example: 'brand' })
|
||||
@Rule(RuleType.string())
|
||||
dictName?: string;
|
||||
|
||||
@ApiProperty({ description: '属性值', example: 'ZYN' })
|
||||
@Rule(RuleType.string())
|
||||
value?: string;
|
||||
|
||||
@ApiProperty({ description: '属性ID', example: 1 })
|
||||
@Rule(RuleType.number())
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ description: '属性名称', example: 'ZYN' })
|
||||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: '属性显示名称', example: 'ZYN' })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建产品
|
||||
*/
|
||||
|
|
@ -21,6 +46,10 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.string())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
|
||||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||
@ApiProperty({ description: '属性列表', type: 'array' })
|
||||
@Rule(RuleType.array().required())
|
||||
|
|
@ -75,6 +104,10 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.string())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
|
||||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||
@Rule(RuleType.number())
|
||||
|
|
@ -96,223 +129,58 @@ export class UpdateProductDTO {
|
|||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建分类属性绑定
|
||||
*/
|
||||
export class CreateCategoryAttributeDTO {
|
||||
@ApiProperty({ description: '分类字典项ID', example: 1 })
|
||||
@Rule(RuleType.number().required())
|
||||
categoryItemId: number;
|
||||
|
||||
@ApiProperty({ description: '属性字典ID列表', example: [2, 3] })
|
||||
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||
attributeDictIds: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于分页查询产品
|
||||
*/
|
||||
export class QueryProductDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
@ApiProperty({ description: '当前页', example: 1 })
|
||||
@Rule(RuleType.number().default(1))
|
||||
current: number;
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
@ApiProperty({ description: '每页数量', example: 10 })
|
||||
@Rule(RuleType.number().default(10))
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: '1', description: '品牌 ID' })
|
||||
@Rule(RuleType.string())
|
||||
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 })
|
||||
@ApiProperty({ description: '搜索关键字', required: false })
|
||||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: '分类ID', required: false })
|
||||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '品牌ID', required: false })
|
||||
@Rule(RuleType.number())
|
||||
brandId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于创建品牌
|
||||
* DTO 用于设置产品组成
|
||||
*/
|
||||
export class CreateBrandDTO {
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌名称', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: '品牌名称不能为空' }))
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌唯一标识', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于更新品牌
|
||||
*/
|
||||
export class UpdateBrandDTO {
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '品牌唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 用于查询品牌(支持分页)
|
||||
*/
|
||||
export class QueryBrandDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
export class CreateFlavorsDTO {
|
||||
@ApiProperty({ example: 'WINTERGREEN', description: '口味名称', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: '口味名称不能为空' }))
|
||||
title: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'WINTERGREEN',
|
||||
description: '口味唯一标识',
|
||||
required: true,
|
||||
})
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateFlavorsDTO {
|
||||
@ApiProperty({ example: 'WINTERGREEN', description: '口味名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 'WINTERGREEN', description: '口味唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class QueryFlavorsDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
export class CreateStrengthDTO {
|
||||
@ApiProperty({ example: '6MG', description: '规格名称', required: false })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ example: '6MG', description: '规格唯一标识', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateStrengthDTO {
|
||||
@ApiProperty({ example: '6MG', description: '规格名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: '6MG', description: '规格唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class QueryStrengthDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: 'YOONE', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
// size 新增 DTO
|
||||
export class CreateSizeDTO {
|
||||
@ApiProperty({ example: '6', description: '尺寸名称', required: false })
|
||||
@Rule(RuleType.string())
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ example: '6', description: '尺寸唯一标识', required: true })
|
||||
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class UpdateSizeDTO {
|
||||
@ApiProperty({ example: '6', description: '尺寸名称' })
|
||||
@Rule(RuleType.string())
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: '6', description: '尺寸唯一标识' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class QuerySizeDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number; // 页码
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number; // 每页大小
|
||||
|
||||
@ApiProperty({ example: '6', description: '关键字' })
|
||||
@Rule(RuleType.string())
|
||||
name: string; // 搜索关键字(支持模糊查询)
|
||||
}
|
||||
|
||||
export class SkuItemDTO {
|
||||
@ApiProperty({ description: '产品 ID' })
|
||||
productId: number;
|
||||
|
||||
@ApiProperty({ description: 'sku 编码' })
|
||||
sku: string;
|
||||
}
|
||||
|
||||
export class BatchSetSkuDTO {
|
||||
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
|
||||
skus: SkuItemDTO[];
|
||||
}
|
||||
|
||||
// 中文注释:产品库存组成项输入
|
||||
export class ProductComponentItemDTO {
|
||||
@ApiProperty({ description: '组件 SKU' })
|
||||
@Rule(RuleType.string().required())
|
||||
sku: string;
|
||||
|
||||
@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[];
|
||||
@ApiProperty({ description: '产品组成', type: 'array', required: true })
|
||||
@Rule(
|
||||
RuleType.array()
|
||||
.items(
|
||||
RuleType.object({
|
||||
sku: RuleType.string().required(),
|
||||
quantity: RuleType.number().required(),
|
||||
})
|
||||
)
|
||||
.required()
|
||||
)
|
||||
components: { sku: string; quantity: number }[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ export class CreateSiteDTO {
|
|||
type?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
}
|
||||
|
||||
export class UpdateSiteDTO {
|
||||
|
|
@ -61,6 +66,11 @@ export class UpdateSiteDTO {
|
|||
type?: string;
|
||||
@Rule(RuleType.string().optional())
|
||||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
}
|
||||
|
||||
export class QuerySiteDTO {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,11 @@ export class CreateStockPointDTO {
|
|||
@ApiProperty()
|
||||
@Rule(RuleType.string())
|
||||
contactPhone: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
}
|
||||
export class UpdateStockPointDTO extends CreateStockPointDTO {}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Product } from './product.entity';
|
||||
import { CategoryAttribute } from './category_attribute.entity';
|
||||
|
||||
@Entity('category')
|
||||
export class Category {
|
||||
@ApiProperty({ description: 'ID' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '分类显示名称' })
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: '分类中文名称' })
|
||||
@Column({ nullable: true })
|
||||
titleCN: string;
|
||||
|
||||
@ApiProperty({ description: '分类唯一标识' })
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '排序' })
|
||||
@Column({ default: 0 })
|
||||
sort: number;
|
||||
|
||||
@OneToMany(() => Product, product => product.category)
|
||||
products: Product[];
|
||||
|
||||
@OneToMany(() => CategoryAttribute, categoryAttribute => categoryAttribute.category)
|
||||
attributes: CategoryAttribute[];
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Category } from './category.entity';
|
||||
import { Dict } from './dict.entity';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
|
||||
@Entity()
|
||||
export class CategoryAttribute {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '分类' })
|
||||
@ManyToOne(() => Category, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'category_id' })
|
||||
category: Category;
|
||||
|
||||
@ApiProperty({ description: '关联的属性字典' })
|
||||
@ManyToOne(() => Dict, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'attribute_dict_id' })
|
||||
attributeDict: Dict;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -7,10 +7,13 @@ import {
|
|||
ManyToMany,
|
||||
JoinTable,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { DictItem } from './dict_item.entity';
|
||||
import { ProductStockComponent } from './product_stock_component.entity';
|
||||
import { Category } from './category.entity';
|
||||
|
||||
@Entity()
|
||||
export class Product {
|
||||
|
|
@ -64,6 +67,12 @@ export class Product {
|
|||
@Column({ default: 0 })
|
||||
stock: number;
|
||||
|
||||
|
||||
// 分类关联
|
||||
@ManyToOne(() => Category, category => category.products)
|
||||
@JoinColumn({ name: 'categoryId' })
|
||||
category: Category;
|
||||
|
||||
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
||||
cascade: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository, Like, In } from 'typeorm';
|
||||
import { Category } from '../entity/category.entity';
|
||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||
import { Dict } from '../entity/dict.entity';
|
||||
|
||||
@Provide()
|
||||
export class CategoryService {
|
||||
@InjectEntityModel(Category)
|
||||
categoryModel: Repository<Category>;
|
||||
|
||||
@InjectEntityModel(CategoryAttribute)
|
||||
categoryAttributeModel: Repository<CategoryAttribute>;
|
||||
|
||||
@InjectEntityModel(Dict)
|
||||
dictModel: Repository<Dict>;
|
||||
|
||||
async getAll() {
|
||||
return await this.categoryModel.find({
|
||||
order: {
|
||||
sort: 'DESC',
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getList(options: { current?: number; pageSize?: number }, name?: string) {
|
||||
const { current = 1, pageSize = 10 } = options;
|
||||
const where = name ? [
|
||||
{ title: Like(`%${name}%`) },
|
||||
{ name: Like(`%${name}%`) }
|
||||
] : [];
|
||||
|
||||
const [list, total] = await this.categoryModel.findAndCount({
|
||||
where: where.length ? where : undefined,
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
order: {
|
||||
sort: 'DESC',
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
async create(data: Partial<Category>) {
|
||||
return await this.categoryModel.save(data);
|
||||
}
|
||||
|
||||
async update(id: number, data: Partial<Category>) {
|
||||
await this.categoryModel.update(id, data);
|
||||
return await this.categoryModel.findOneBy({ id });
|
||||
}
|
||||
|
||||
async delete(id: number) {
|
||||
return await this.categoryModel.delete(id);
|
||||
}
|
||||
|
||||
async findByName(name: string) {
|
||||
return await this.categoryModel.findOneBy({ name });
|
||||
}
|
||||
|
||||
async getCategoryAttributes(categoryId: number) {
|
||||
const categoryAttributes = await this.categoryAttributeModel.find({
|
||||
where: { category: { id: categoryId } },
|
||||
relations: ['attributeDict', 'category'],
|
||||
});
|
||||
|
||||
return categoryAttributes.map(ca => ca.attributeDict);
|
||||
}
|
||||
|
||||
async createCategoryAttribute(categoryId: number, attributeDictIds: number[]) {
|
||||
const category = await this.categoryModel.findOneBy({ id: categoryId });
|
||||
if (!category) throw new Error('分类不存在');
|
||||
|
||||
const dicts = await this.dictModel.findBy({ id: In(attributeDictIds) });
|
||||
if (dicts.length !== attributeDictIds.length) throw new Error('部分属性字典不存在');
|
||||
|
||||
// 检查是否已存在
|
||||
const exist = await this.categoryAttributeModel.find({
|
||||
where: {
|
||||
category: { id: categoryId },
|
||||
attributeDict: { id: In(attributeDictIds) }
|
||||
},
|
||||
relations: ['attributeDict']
|
||||
});
|
||||
|
||||
const existIds = exist.map(e => e.attributeDict.id);
|
||||
const newIds = attributeDictIds.filter(id => !existIds.includes(id));
|
||||
|
||||
const newRecords = newIds.map(id => {
|
||||
const record = new CategoryAttribute();
|
||||
record.category = category;
|
||||
record.attributeDict = dicts.find(d => d.id === id);
|
||||
return record;
|
||||
});
|
||||
|
||||
return await this.categoryAttributeModel.save(newRecords);
|
||||
}
|
||||
|
||||
async deleteCategoryAttribute(id: number) {
|
||||
return await this.categoryAttributeModel.delete(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,16 +4,8 @@ import { Product } from '../entity/product.entity';
|
|||
import { paginate } from '../utils/paginate.util';
|
||||
import { PaginationParams } from '../interface';
|
||||
import {
|
||||
CreateBrandDTO,
|
||||
CreateFlavorsDTO,
|
||||
CreateProductDTO,
|
||||
CreateStrengthDTO,
|
||||
CreateSizeDTO,
|
||||
UpdateBrandDTO,
|
||||
UpdateFlavorsDTO,
|
||||
UpdateProductDTO,
|
||||
UpdateStrengthDTO,
|
||||
UpdateSizeDTO,
|
||||
} from '../dto/product.dto';
|
||||
import {
|
||||
BrandPaginatedResponse,
|
||||
|
|
@ -33,6 +25,7 @@ import { StockService } from './stock.service';
|
|||
import { Stock } from '../entity/stock.entity';
|
||||
import { StockPoint } from '../entity/stock_point.entity';
|
||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||
import { Category } from '../entity/category.entity';
|
||||
|
||||
@Provide()
|
||||
export class ProductService {
|
||||
|
|
@ -69,6 +62,17 @@ export class ProductService {
|
|||
@InjectEntityModel(ProductStockComponent)
|
||||
productStockComponentModel: Repository<ProductStockComponent>;
|
||||
|
||||
@InjectEntityModel(Category)
|
||||
categoryModel: Repository<Category>;
|
||||
|
||||
|
||||
// 获取所有 WordPress 商品
|
||||
async getWpProducts() {
|
||||
return this.wpProductModel.find();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// async findProductsByName(name: string): Promise<Product[]> {
|
||||
// const where: any = {};
|
||||
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
|
|
@ -89,7 +93,8 @@ export class ProductService {
|
|||
|
||||
async findProductsByName(name: string): Promise<Product[]> {
|
||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
const query = this.productModel.createQueryBuilder('product');
|
||||
const query = this.productModel.createQueryBuilder('product')
|
||||
.leftJoinAndSelect('product.category', 'category');
|
||||
|
||||
// 保证 sku 不为空
|
||||
query.where('product.sku IS NOT NULL');
|
||||
|
|
@ -128,6 +133,7 @@ export class ProductService {
|
|||
where: {
|
||||
sku,
|
||||
},
|
||||
relations: ['category', 'attributes', 'attributes.dict']
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +145,8 @@ export class ProductService {
|
|||
const qb = this.productModel
|
||||
.createQueryBuilder('product')
|
||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||
.leftJoinAndSelect('attribute.dict', 'dict');
|
||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||
.leftJoinAndSelect('product.category', 'category');
|
||||
|
||||
// 模糊搜索 name,支持多个关键词
|
||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
|
|
@ -231,16 +238,41 @@ export class ProductService {
|
|||
}
|
||||
|
||||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||||
const { name, description, attributes, sku, price } = createProductDTO;
|
||||
const { name, description, attributes, sku, price, categoryId } = createProductDTO;
|
||||
|
||||
// 条件判断(中文注释:校验属性输入)
|
||||
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||||
// 如果提供了 categoryId 但没有 attributes,初始化为空数组
|
||||
if (!attributes && categoryId) {
|
||||
// 继续执行,下面会处理 categoryId
|
||||
} else {
|
||||
throw new Error('属性列表不能为空');
|
||||
}
|
||||
}
|
||||
|
||||
const safeAttributes = attributes || [];
|
||||
|
||||
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
|
||||
const resolvedAttributes: DictItem[] = [];
|
||||
for (const attr of attributes) {
|
||||
let categoryItem: Category | null = null;
|
||||
|
||||
// 如果提供了 categoryId,设置分类
|
||||
if (categoryId) {
|
||||
categoryItem = await this.categoryModel.findOne({ where: { id: categoryId } });
|
||||
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
|
||||
}
|
||||
|
||||
for (const attr of safeAttributes) {
|
||||
// 中文注释:如果属性是分类,特殊处理
|
||||
if (attr.dictName === 'category') {
|
||||
if (attr.id) {
|
||||
categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
|
||||
} else if (attr.name) {
|
||||
categoryItem = await this.categoryModel.findOneBy({ name: attr.name });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let item: DictItem | null = null;
|
||||
if (attr.id) {
|
||||
// 中文注释:如果传入了 id,直接查找字典项并使用,不强制要求 dictName
|
||||
|
|
@ -274,6 +306,9 @@ export class ProductService {
|
|||
product.name = name;
|
||||
product.description = description;
|
||||
product.attributes = resolvedAttributes;
|
||||
if (categoryItem) {
|
||||
product.category = categoryItem;
|
||||
}
|
||||
// 条件判断(中文注释:设置商品类型,默认 simple)
|
||||
product.type = (createProductDTO.type as any) || 'single';
|
||||
|
||||
|
|
@ -310,7 +345,7 @@ export class ProductService {
|
|||
updateProductDTO: UpdateProductDTO
|
||||
): Promise<Product> {
|
||||
// 检查产品是否存在(包含属性关系)
|
||||
const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict'] });
|
||||
const product = await this.productModel.findOne({ where: { id }, relations: ['attributes', 'attributes.dict', 'category'] });
|
||||
if (!product) {
|
||||
throw new Error(`产品 ID ${id} 不存在`);
|
||||
}
|
||||
|
|
@ -322,6 +357,16 @@ export class ProductService {
|
|||
if (updateProductDTO.description !== undefined) {
|
||||
product.description = updateProductDTO.description;
|
||||
}
|
||||
if (updateProductDTO.categoryId !== undefined) {
|
||||
if (updateProductDTO.categoryId) {
|
||||
const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } });
|
||||
if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`);
|
||||
product.category = categoryItem;
|
||||
} else {
|
||||
// 如果传了 0 或 null,可以清除分类(根据需求)
|
||||
// product.category = null;
|
||||
}
|
||||
}
|
||||
if (updateProductDTO.price !== undefined) {
|
||||
product.price = Number(updateProductDTO.price);
|
||||
}
|
||||
|
|
@ -350,6 +395,15 @@ export class ProductService {
|
|||
};
|
||||
|
||||
for (const attr of updateProductDTO.attributes) {
|
||||
// 中文注释:如果属性是分类,特殊处理
|
||||
if (attr.dictName === 'category') {
|
||||
if (attr.id) {
|
||||
const categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
|
||||
if (categoryItem) product.category = categoryItem;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let item: DictItem | null = null;
|
||||
if (attr.id) {
|
||||
// 中文注释:当提供 id 时直接查询字典项,不强制要求 dictName
|
||||
|
|
@ -629,7 +683,7 @@ export class ProductService {
|
|||
return this.dictItemModel.find({ where: { dict: { id: brandDict.id } } });
|
||||
}
|
||||
|
||||
async createBrand(createBrandDTO: CreateBrandDTO): Promise<DictItem> {
|
||||
async createBrand(createBrandDTO: any): Promise<DictItem> {
|
||||
const { title, name } = createBrandDTO;
|
||||
|
||||
// 查找 'brand' 字典
|
||||
|
|
@ -652,7 +706,7 @@ export class ProductService {
|
|||
return await this.dictItemModel.save(brand);
|
||||
}
|
||||
|
||||
async updateBrand(id: number, updateBrand: UpdateBrandDTO) {
|
||||
async updateBrand(id: number, updateBrand: any) {
|
||||
// 确认品牌是否存在
|
||||
const brand = await this.dictItemModel.findOneBy({ id });
|
||||
if (!brand) {
|
||||
|
|
@ -712,7 +766,7 @@ export class ProductService {
|
|||
return this.dictItemModel.find({ where: { dict: { id: flavorsDict.id } } });
|
||||
}
|
||||
|
||||
async createFlavors(createFlavorsDTO: CreateFlavorsDTO): Promise<DictItem> {
|
||||
async createFlavors(createFlavorsDTO: any): Promise<DictItem> {
|
||||
const { title, name } = createFlavorsDTO;
|
||||
const flavorsDict = await this.dictModel.findOne({
|
||||
where: { name: 'flavor' },
|
||||
|
|
@ -727,7 +781,7 @@ export class ProductService {
|
|||
return await this.dictItemModel.save(flavors);
|
||||
}
|
||||
|
||||
async updateFlavors(id: number, updateFlavors: UpdateFlavorsDTO) {
|
||||
async updateFlavors(id: number, updateFlavors: any) {
|
||||
const flavors = await this.dictItemModel.findOneBy({ id });
|
||||
if (!flavors) {
|
||||
throw new Error(`口味 ID ${id} 不存在`);
|
||||
|
|
@ -779,7 +833,7 @@ export class ProductService {
|
|||
return this.dictItemModel.find({ where: { dict: { id: sizeDict.id } } }) as any;
|
||||
}
|
||||
|
||||
async createSize(createSizeDTO: CreateSizeDTO): Promise<DictItem> {
|
||||
async createSize(createSizeDTO: any): Promise<DictItem> {
|
||||
const { title, name } = createSizeDTO;
|
||||
// 获取 size 字典(中文注释:用于挂载尺寸项)
|
||||
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
|
||||
|
|
@ -795,7 +849,7 @@ export class ProductService {
|
|||
return await this.dictItemModel.save(size);
|
||||
}
|
||||
|
||||
async updateSize(id: number, updateSize: UpdateSizeDTO) {
|
||||
async updateSize(id: number, updateSize: any) {
|
||||
// 先查询(中文注释:确保尺寸项存在)
|
||||
const size = await this.dictItemModel.findOneBy({ id });
|
||||
// 条件判断(中文注释:不存在则报错)
|
||||
|
|
@ -866,7 +920,7 @@ export class ProductService {
|
|||
return this.dictItemModel.find({ where: { dict: { id: strengthDict.id } } });
|
||||
}
|
||||
|
||||
async createStrength(createStrengthDTO: CreateStrengthDTO): Promise<DictItem> {
|
||||
async createStrength(createStrengthDTO: any): Promise<DictItem> {
|
||||
const { title, name } = createStrengthDTO;
|
||||
const strengthDict = await this.dictModel.findOne({
|
||||
where: { name: 'strength' },
|
||||
|
|
@ -950,7 +1004,7 @@ export class ProductService {
|
|||
await this.dictItemModel.delete({ id });
|
||||
}
|
||||
|
||||
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
|
||||
async updateStrength(id: number, updateStrength: any) {
|
||||
const strength = await this.dictItemModel.findOneBy({ id });
|
||||
if (!strength) {
|
||||
throw new Error(`规格 ID ${id} 不存在`);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
|
|||
import { Repository, Like, In } from 'typeorm';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { WpSite } from '../interface';
|
||||
import { UpdateSiteDTO } from '../dto/site.dto';
|
||||
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||
import { Area } from '../entity/area.entity';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
|
|
@ -11,11 +12,16 @@ export class SiteService {
|
|||
@InjectEntityModel(Site)
|
||||
siteModel: Repository<Site>;
|
||||
|
||||
@InjectEntityModel(Area)
|
||||
areaModel: Repository<Area>;
|
||||
|
||||
async syncFromConfig(sites: WpSite[] = []) {
|
||||
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
||||
for (const siteConfig of sites) {
|
||||
// 按站点名称查询是否已存在记录
|
||||
const exist = await this.siteModel.findOne({ where: { name: siteConfig.name } });
|
||||
const exist = await this.siteModel.findOne({
|
||||
where: { name: siteConfig.name },
|
||||
});
|
||||
// 将 WpSite 字段映射为 Site 实体字段
|
||||
const payload: Partial<Site> = {
|
||||
name: siteConfig.name,
|
||||
|
|
@ -25,63 +31,142 @@ export class SiteService {
|
|||
type: 'woocommerce',
|
||||
};
|
||||
// 存在则更新,不存在则插入新记录
|
||||
if (exist) await this.siteModel.update({ id: exist.id }, payload);
|
||||
else await this.siteModel.insert(payload as Site);
|
||||
if (exist) {
|
||||
await this.siteModel.update({ id: exist.id }, payload);
|
||||
} else {
|
||||
await this.siteModel.insert(payload as Site);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create(data: Partial<Site>) {
|
||||
// 创建新的站点记录
|
||||
await this.siteModel.insert(data as Site);
|
||||
async create(data: CreateSiteDTO) {
|
||||
// 从 DTO 中分离出区域代码和其他站点数据
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
const newSite = new Site();
|
||||
Object.assign(newSite, restData);
|
||||
|
||||
// 如果传入了区域代码,则查询并关联 Area 实体
|
||||
if (areaCodes && areaCodes.length > 0) {
|
||||
const areas = await this.areaModel.findBy({
|
||||
code: In(areaCodes),
|
||||
});
|
||||
newSite.areas = areas;
|
||||
} else {
|
||||
// 如果没有传入区域,则关联一个空数组,代表“全局”
|
||||
newSite.areas = [];
|
||||
}
|
||||
|
||||
// 使用 save 方法保存实体及其关联关系
|
||||
await this.siteModel.save(newSite);
|
||||
return true;
|
||||
}
|
||||
|
||||
async update(id: string | number, data: UpdateSiteDTO) {
|
||||
// 更新指定站点记录,将布尔 isDisabled 转换为数值 0/1
|
||||
// 从 DTO 中分离出区域代码和其他站点数据
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
|
||||
// 首先,根据 ID 查找要更新的站点实体
|
||||
const siteToUpdate = await this.siteModel.findOne({
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
if (!siteToUpdate) {
|
||||
// 如果找不到站点,则操作失败
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新站点的基本字段
|
||||
const payload: Partial<Site> = {
|
||||
...data,
|
||||
...restData,
|
||||
isDisabled:
|
||||
data.isDisabled === undefined // 未传入则不更新该字段
|
||||
data.isDisabled === undefined
|
||||
? undefined
|
||||
: data.isDisabled // true -> 1, false -> 0
|
||||
: data.isDisabled
|
||||
? 1
|
||||
: 0,
|
||||
} as any;
|
||||
await this.siteModel.update({ id: Number(id) }, payload);
|
||||
Object.assign(siteToUpdate, payload);
|
||||
|
||||
// 如果 DTO 中传入了 areas 字段(即使是空数组),也要更新关联关系
|
||||
if (areaCodes !== undefined) {
|
||||
if (areaCodes.length > 0) {
|
||||
// 如果区域代码数组不为空,则查找并更新关联
|
||||
const areas = await this.areaModel.findBy({
|
||||
code: In(areaCodes),
|
||||
});
|
||||
siteToUpdate.areas = areas;
|
||||
} else {
|
||||
// 如果传入空数组,则清空所有关联,代表“全局”
|
||||
siteToUpdate.areas = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 save 方法保存实体及其更新后的关联关系
|
||||
await this.siteModel.save(siteToUpdate);
|
||||
return true;
|
||||
}
|
||||
|
||||
async get(id: string | number, includeSecret = false) {
|
||||
// 根据主键获取站点;includeSecret 为 true 时返回密钥字段
|
||||
const site = await this.siteModel.findOne({ where: { id: Number(id) } });
|
||||
if (!site) return null;
|
||||
if (includeSecret) return site;
|
||||
// 根据主键获取站点,并使用 relations 加载关联的 areas
|
||||
const site = await this.siteModel.findOne({
|
||||
where: { id: Number(id) },
|
||||
relations: ['areas'],
|
||||
});
|
||||
if (!site) {
|
||||
return null;
|
||||
}
|
||||
// 如果需要包含密钥,则直接返回
|
||||
if (includeSecret) {
|
||||
return site;
|
||||
}
|
||||
// 默认不返回密钥,进行字段脱敏
|
||||
const { consumerKey, consumerSecret, ...rest } = site;
|
||||
return rest;
|
||||
}
|
||||
|
||||
async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) {
|
||||
async list(
|
||||
param: {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
isDisabled?: boolean;
|
||||
ids?: string;
|
||||
},
|
||||
includeSecret = false
|
||||
) {
|
||||
// 分页查询站点列表,支持关键字、禁用状态与 ID 列表过滤
|
||||
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any;
|
||||
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param ||
|
||||
{}) as any;
|
||||
const where: any = {};
|
||||
// 按名称模糊查询
|
||||
if (keyword) where.name = Like(`%${keyword}%`);
|
||||
if (keyword) {
|
||||
where.name = Like(`%${keyword}%`);
|
||||
}
|
||||
// 按禁用状态过滤(布尔转数值)
|
||||
if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0;
|
||||
if (typeof isDisabled === 'boolean') {
|
||||
where.isDisabled = isDisabled ? 1 : 0;
|
||||
}
|
||||
if (ids) {
|
||||
// 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
|
||||
const numIds = String(ids)
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.map((i) => Number(i))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
if (numIds.length > 0) where.id = In(numIds);
|
||||
.map(i => Number(i))
|
||||
.filter(v => !Number.isNaN(v));
|
||||
if (numIds.length > 0) {
|
||||
where.id = In(numIds);
|
||||
}
|
||||
// 进行分页查询(skip/take)并返回总条数
|
||||
const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize });
|
||||
}
|
||||
// 进行分页查询,并使用 relations 加载关联的 areas
|
||||
const [items, total] = await this.siteModel.findAndCount({
|
||||
where,
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
relations: ['areas'],
|
||||
});
|
||||
// 根据 includeSecret 决定是否脱敏返回密钥字段
|
||||
const data = includeSecret ? items : items.map((item: any) => {
|
||||
const data = includeSecret
|
||||
? items
|
||||
: items.map((item: any) => {
|
||||
const { consumerKey, consumerSecret, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { Between, Like, Repository, LessThan, MoreThan } from 'typeorm';
|
||||
import { Between, Like, Repository, LessThan, MoreThan, In } from 'typeorm';
|
||||
import { Stock } from '../entity/stock.entity';
|
||||
import { StockRecord } from '../entity/stock_record.entity';
|
||||
import { paginate } from '../utils/paginate.util';
|
||||
|
|
@ -27,6 +27,7 @@ import { User } from '../entity/user.entity';
|
|||
import dayjs = require('dayjs');
|
||||
import { Transfer } from '../entity/transfer.entity';
|
||||
import { TransferItem } from '../entity/transfer_item.entity';
|
||||
import { Area } from '../entity/area.entity';
|
||||
|
||||
@Provide()
|
||||
export class StockService {
|
||||
|
|
@ -51,35 +52,55 @@ export class StockService {
|
|||
@InjectEntityModel(TransferItem)
|
||||
transferItemModel: Repository<TransferItem>;
|
||||
|
||||
@InjectEntityModel(Area)
|
||||
areaModel: Repository<Area>;
|
||||
|
||||
async createStockPoint(data: CreateStockPointDTO) {
|
||||
const { name, location, contactPerson, contactPhone } = data;
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
const stockPoint = new StockPoint();
|
||||
stockPoint.name = name;
|
||||
stockPoint.location = location;
|
||||
stockPoint.contactPerson = contactPerson;
|
||||
stockPoint.contactPhone = contactPhone;
|
||||
Object.assign(stockPoint, restData);
|
||||
|
||||
if (areaCodes && areaCodes.length > 0) {
|
||||
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
|
||||
stockPoint.areas = areas;
|
||||
} else {
|
||||
stockPoint.areas = [];
|
||||
}
|
||||
|
||||
await this.stockPointModel.save(stockPoint);
|
||||
}
|
||||
|
||||
async updateStockPoint(id: number, data: UpdateStockPointDTO) {
|
||||
// 确认产品是否存在
|
||||
const point = await this.stockPointModel.findOneBy({ id });
|
||||
if (!point) {
|
||||
throw new Error(`产品 ID ${id} 不存在`);
|
||||
const { areas: areaCodes, ...restData } = data;
|
||||
const pointToUpdate = await this.stockPointModel.findOneBy({ id });
|
||||
if (!pointToUpdate) {
|
||||
throw new Error(`仓库点 ID ${id} 不存在`);
|
||||
}
|
||||
// 更新产品
|
||||
await this.stockPointModel.update(id, data);
|
||||
|
||||
Object.assign(pointToUpdate, restData);
|
||||
|
||||
if (areaCodes !== undefined) {
|
||||
if (areaCodes.length > 0) {
|
||||
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
|
||||
pointToUpdate.areas = areas;
|
||||
} else {
|
||||
pointToUpdate.areas = [];
|
||||
}
|
||||
}
|
||||
|
||||
await this.stockPointModel.save(pointToUpdate);
|
||||
}
|
||||
|
||||
async getStockPoints(query: QueryPointDTO) {
|
||||
const { current = 1, pageSize = 10 } = query;
|
||||
return await paginate(this.stockPointModel, {
|
||||
pagination: { current, pageSize },
|
||||
relations: ['areas'],
|
||||
});
|
||||
}
|
||||
|
||||
async getAllStockPoints(): Promise<StockPoint[]> {
|
||||
return await this.stockPointModel.find();
|
||||
return await this.stockPointModel.find({ relations: ['areas'] });
|
||||
}
|
||||
|
||||
async delStockPoints(id: number) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
import { App, IMidwayApplication, Inject } from '@midwayjs/core';
|
||||
import { Framework } from '@midwayjs/koa';
|
||||
import { Bootstrap } from '@midwayjs/bootstrap';
|
||||
import { Product } from './src/entity/product.entity';
|
||||
import { WpProduct } from './src/entity/wp_product.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
|
||||
@App()
|
||||
class TestApp {
|
||||
@InjectEntityModel(Product)
|
||||
productModel: Repository<Product>;
|
||||
|
||||
@InjectEntityModel(WpProduct)
|
||||
wpProductModel: Repository<WpProduct>;
|
||||
|
||||
async run() {
|
||||
if (!this.productModel || !this.wpProductModel) {
|
||||
console.log('Models not injected');
|
||||
return;
|
||||
}
|
||||
const productCount = await this.productModel.count();
|
||||
const wpProductCount = await this.wpProductModel.count();
|
||||
console.log(`Product Count: ${productCount}`);
|
||||
console.log(`WpProduct Count: ${wpProductCount}`);
|
||||
}
|
||||
}
|
||||
|
||||
Bootstrap.run().then(async () => {
|
||||
const app = await Bootstrap.getApplication() as IMidwayApplication<any>;
|
||||
const testApp = await app.getApplicationContext().getAsync(TestApp);
|
||||
await testApp.run();
|
||||
process.exit(0);
|
||||
});
|
||||
Loading…
Reference in New Issue