feat: 添加分类管理和区域关联功能

实现分类实体、控制器和服务,支持分类与产品的关联
在站点和仓库点DTO中添加区域字段,支持区域关联查询
更新数据库配置和种子数据,优化产品属性管理
This commit is contained in:
tikkhun 2025-12-02 15:02:45 +08:00
parent f20f4727f6
commit 0180360519
23 changed files with 888 additions and 359 deletions

1
scripts/create_db.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

21
scripts/create_db.js Normal file
View File

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

1
scripts/diagnose_db.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

31
scripts/diagnose_db.js Normal file
View File

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

View File

@ -36,7 +36,11 @@ import { DictItem } from '../entity/dict_item.entity';
import { Template } from '../entity/template.entity'; import { Template } from '../entity/template.entity';
import { Area } from '../entity/area.entity'; import { Area } from '../entity/area.entity';
import { ProductStockComponent } from '../entity/product_stock_component.entity'; import { 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 DictSeeder from '../db/seeds/dict.seeder';
import CategorySeeder from '../db/seeds/category.seeder';
import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder';
export default { export default {
// use for cookie sign key, should change to your own and keep security // use for cookie sign key, should change to your own and keep security
@ -81,16 +85,18 @@ export default {
DictItem, DictItem,
Template, Template,
Area, Area,
CategoryAttribute,
Category,
], ],
synchronize: true, synchronize: true,
logging: false, logging: false,
seeders: [DictSeeder], seeders: [DictSeeder, CategorySeeder, CategoryAttributeSeeder],
}, },
dataSource: { dataSource: {
default: { default: {
type: 'mysql', type: 'mysql',
host: 'localhost', host: 'localhost',
port: 3306, port: 10014,
username: 'root', username: 'root',
password: 'root', password: 'root',
database: 'inventory', database: 'inventory',

View File

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

View File

@ -21,7 +21,6 @@ import { Context } from '@midwayjs/koa';
export class ProductController { export class ProductController {
@Inject() @Inject()
productService: ProductService; productService: ProductService;
ProductRes;
@Inject() @Inject()
ctx: Context; ctx: Context;
@ -171,7 +170,7 @@ export class ProductController {
@Post('/:id/components') @Post('/:id/components')
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) { async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
try { try {
const data = await this.productService.setProductComponents(id, body?.items || []); const data = await this.productService.setProductComponents(id, body?.components || []);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || 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() @ApiOkResponse()
@Get('/attribute') @Get('/attribute')

View File

@ -4,12 +4,13 @@ import { SeederOptions } from 'typeorm-extension';
const options: DataSourceOptions & SeederOptions = { const options: DataSourceOptions & SeederOptions = {
type: 'mysql', type: 'mysql',
host: 'localhost', // host: 'localhost',
port: 23306, // port: 10014,
socketPath: '/Users/zksu/Library/Application Support/Local/run/oLbUT7qMU/mysql/mysqld.sock',
username: 'root', username: 'root',
password: '12345678', password: 'root',
database: 'inventory', database: 'inventory',
synchronize: false, synchronize: true,
logging: true, logging: true,
entities: [__dirname + '/../entity/*.ts'], entities: [__dirname + '/../entity/*.ts'],
migrations: ['src/db/migrations/**/*.ts'], migrations: ['src/db/migrations/**/*.ts'],

View File

@ -11,15 +11,20 @@ export default class AreaSeeder implements Seeder {
const areaRepository = dataSource.getRepository(Area); const areaRepository = dataSource.getRepository(Area);
const areas = [ const areas = [
{ name: 'Australia' }, { name: 'Australia', code: 'AU' },
{ name: 'Canada' }, { name: 'Canada', code: 'CA' },
{ name: 'United States' }, { name: 'United States', code: 'US' },
{ name: 'Germany' }, { name: 'Germany', code: 'DE' },
{ name: 'Poland' }, { name: 'Poland', code: 'PL' },
]; ];
for (const areaData of areas) { 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) { if (!existingArea) {
const newArea = areaRepository.create(areaData); const newArea = areaRepository.create(areaData);
await areaRepository.save(newArea); await areaRepository.save(newArea);

View File

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

View File

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

View File

@ -22,79 +22,77 @@ export default class DictSeeder implements Seeder {
const dictItemRepository = dataSource.getRepository(DictItem); const dictItemRepository = dataSource.getRepository(DictItem);
const flavorsData = [ const flavorsData = [
{ title: 'Bellini', name: 'bellini' }, { name: 'bellini', title: 'Bellini', titleCn: '贝利尼' },
{ title: 'Max Polarmint', name: 'max-polarmint' }, { name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷' },
{ title: 'Blueberry', name: 'blueberry' }, { name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓' },
{ title: 'Citrus', name: 'citrus' }, { name: 'citrus', title: 'Citrus', titleCn: '柑橘' },
{ title: 'Wintergreen', name: 'wintergreen' }, { name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷' },
{ title: 'COOL MINT', name: 'cool-mint' }, { name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷' },
{ title: 'JUICY PEACH', name: 'juicy-peach' }, { name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃' },
{ title: 'ORANGE', name: 'orange' }, { name: 'orange', title: 'ORANGE', titleCn: '橙子' },
{ title: 'PEPPERMINT', name: 'peppermint' }, { name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷' },
{ title: 'SPEARMINT', name: 'spearmint' }, { name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷' },
{ title: 'STRAWBERRY', name: 'strawberry' }, { name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓' },
{ title: 'WATERMELON', name: 'watermelon' }, { name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜' },
{ title: 'COFFEE', name: 'coffee' }, { name: 'coffee', title: 'COFFEE', titleCn: '咖啡' },
{ title: 'LEMONADE', name: 'lemonade' }, { name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水' },
{ title: 'apple mint', name: 'apple-mint' }, { name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷' },
{ title: 'PEACH', name: 'peach' }, { name: 'peach', title: 'PEACH', titleCn: '桃子' },
{ title: 'Mango', name: 'mango' }, { name: 'mango', title: 'Mango', titleCn: '芒果' },
{ title: 'ICE WINTERGREEN', name: 'ice-wintergreen' }, { name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷' },
{ title: 'Pink Lemonade', name: 'pink-lemonade' }, { name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水' },
{ title: 'Blackcherry', name: 'blackcherry' }, { name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃' },
{ title: 'fresh mint', name: 'fresh-mint' }, { name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷' },
{ title: 'Strawberry Lychee', name: 'strawberry-lychee' }, { name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝' },
{ title: 'Passion Fruit', name: 'passion-fruit' }, { name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果' },
{ title: 'Banana lce', name: 'banana-lce' }, { name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰' },
{ title: 'Bubblegum', name: 'bubblegum' }, { name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖' },
{ title: 'Mango lce', name: 'mango-lce' }, { name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰' },
{ title: 'Grape lce', name: 'grape-lce' }, { name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰' },
{ title: 'apple', name: 'apple' }, { name: 'apple', title: 'apple', titleCn: '苹果' },
{ title: 'grape', name: 'grape' }, { name: 'grape', title: 'grape', titleCn: '葡萄' },
{ title: 'cherry', name: 'cherry' }, { name: 'cherry', title: 'cherry', titleCn: '樱桃' },
{ title: 'lemon', name: 'lemon' }, { name: 'lemon', title: 'lemon', titleCn: '柠檬' },
{ title: 'razz', name: 'razz' }, { name: 'razz', title: 'razz', titleCn: '覆盆子' },
{ title: 'pineapple', name: 'pineapple' }, { name: 'pineapple', title: 'pineapple', titleCn: '菠萝' },
{ title: 'berry', name: 'berry' }, { name: 'berry', title: 'berry', titleCn: '浆果' },
{ title: 'fruit', name: 'fruit' }, { name: 'fruit', title: 'fruit', titleCn: '水果' },
{ title: 'mint', name: 'mint' }, { name: 'mint', title: 'mint', titleCn: '薄荷' },
{ title: 'menthol', name: 'menthol' }, { name: 'menthol', title: 'menthol', titleCn: '薄荷醇' },
]; ];
const brandsData = [ const brandsData = [
{ title: 'Yoone', name: 'yoone' }, { name: 'yoone', title: 'Yoone', titleCn: '' },
{ title: 'White Fox', name: 'white-fox' }, { name: 'white-fox', title: 'White Fox', titleCn: '' },
{ title: 'ZYN', name: 'zyn' }, { name: 'zyn', title: 'ZYN', titleCn: '' },
{ title: 'Zonnic', name: 'zonnic' }, { name: 'zonnic', title: 'Zonnic', titleCn: '' },
{ title: 'Zolt', name: 'zolt' }, { name: 'zolt', title: 'Zolt', titleCn: '' },
{ title: 'Velo', name: 'velo' }, { name: 'velo', title: 'Velo', titleCn: '' },
{ title: 'Lucy', name: 'lucy' }, { name: 'lucy', title: 'Lucy', titleCn: '' },
{ title: 'EGP', name: 'egp' }, { name: 'egp', title: 'EGP', titleCn: '' },
{ title: 'Bridge', name: 'bridge' }, { name: 'bridge', title: 'Bridge', titleCn: '' },
{ title: 'ZEX', name: 'zex' }, { name: 'zex', title: 'ZEX', titleCn: '' },
{ title: 'Sesh', name: 'sesh' }, { name: 'sesh', title: 'Sesh', titleCn: '' },
{ title: 'Pablo', name: 'pablo' }, { name: 'pablo', title: 'Pablo', titleCn: '' },
]; ];
const strengthsData = [ const strengthsData = [
{ title: '3MG', name: '3mg' }, { name: '2mg', title: '2MG', titleCn: '2毫克' },
{ title: '9MG', name: '9mg' }, { name: '4mg', title: '4MG', titleCn: '4毫克' },
{ title: '2MG', name: '2mg' }, { name: '3mg', title: '3MG', titleCn: '3毫克' },
{ title: '4MG', name: '4mg' }, { name: '6mg', title: '6MG', titleCn: '6毫克' },
{ title: '12MG', name: '12mg' }, { name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克' },
{ title: '18MG', name: '18mg' }, { name: '9mg', title: '9MG', titleCn: '9毫克' },
{ title: '6MG', name: '6mg' }, { name: '12mg', title: '12MG', titleCn: '12毫克' },
{ title: '16.5MG', name: '16-5mg' }, { name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克' },
{ title: '6.5MG', name: '6-5mg' }, { name: '18mg', title: '18MG', titleCn: '18毫克' },
{ title: '30MG', name: '30mg' }, { name: '30mg', title: '30MG', titleCn: '30毫克' },
]; ];
const nonFlavorTokensData = ['slim', 'pouches', 'pouch', 'mini', 'dry'].map(item => ({ title: item, name: item }));
// 初始化语言字典 // 初始化语言字典
const locales = [ const locales = [
{ name: 'zh-cn', title: '简体中文' }, { name: 'zh-cn', title: '简体中文', titleCn: '简体中文' },
{ name: 'en-us', title: 'English' }, { name: 'en-us', title: 'English', titleCn: '英文' },
]; ];
for (const locale of locales) { 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 } } }); let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
if (!item) { 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 } } }); item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
if (!item) { 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 brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌' });
const flavorDict = await this.createOrFindDict(dictRepository, { title: '口味', name: 'flavor' }); const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味' });
const strengthDict = await this.createOrFindDict(dictRepository, { title: '强度', name: 'strength' }); const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度' });
const nonFlavorTokensDict = await this.createOrFindDict(dictRepository, { title: '非口味关键词', name: 'non-flavor-tokens' });
// 遍历品牌数据 // 遍历品牌数据
await this.seedDictItems(dictItemRepository, brandDict, brandsData); 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, strengthDict, strengthsData);
// 遍历非口味关键词数据
await this.seedDictItems(dictItemRepository, nonFlavorTokensDict, nonFlavorTokensData);
} }
/** /**
@ -150,13 +144,13 @@ export default class DictSeeder implements Seeder {
* @param dictInfo * @param dictInfo
* @returns Dict * @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 // 格式化 name
const formattedName = this.formatName(dictInfo.name); const formattedName = this.formatName(dictInfo.name);
let dict = await repo.findOne({ where: { name: formattedName } }); let dict = await repo.findOne({ where: { name: formattedName } });
if (!dict) { if (!dict) {
// 如果字典不存在,则使用格式化后的 name 创建新字典 // 如果字典不存在,则使用格式化后的 name 创建新字典
dict = await repo.save({ title: dictInfo.title, name: formattedName }); dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn });
} }
return dict; return dict;
} }
@ -167,14 +161,14 @@ export default class DictSeeder implements Seeder {
* @param dict * @param dict
* @param items * @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) { for (const item of items) {
// 格式化 name // 格式化 name
const formattedName = this.formatName(item.name); const formattedName = this.formatName(item.name);
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } }); const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
if (!existingItem) { if (!existingItem) {
// 如果字典项不存在,则使用格式化后的 name 创建新字典项 // 如果字典项不存在,则使用格式化后的 name 创建新字典项
await repo.save({ ...item, name: formattedName, dict }); await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, dict });
} }
} }
} }

View File

@ -1,6 +1,31 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate'; 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 * DTO
*/ */
@ -21,6 +46,10 @@ export class CreateProductDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
sku?: string; sku?: string;
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
@Rule(RuleType.number())
categoryId?: number;
// 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等) // 通用属性输入(中文注释:通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
@ApiProperty({ description: '属性列表', type: 'array' }) @ApiProperty({ description: '属性列表', type: 'array' })
@Rule(RuleType.array().required()) @Rule(RuleType.array().required())
@ -75,6 +104,10 @@ export class UpdateProductDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
sku?: string; sku?: string;
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
@Rule(RuleType.number())
categoryId?: number;
// 商品价格 // 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false }) @ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number()) @Rule(RuleType.number())
@ -96,223 +129,58 @@ export class UpdateProductDTO {
type?: string; 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 * DTO
*/ */
export class QueryProductDTO { export class QueryProductDTO {
@ApiProperty({ example: '1', description: '页码' }) @ApiProperty({ description: '当前页', example: 1 })
@Rule(RuleType.number()) @Rule(RuleType.number().default(1))
current: number; current: number;
@ApiProperty({ example: '10', description: '每页大小' }) @ApiProperty({ description: '每页数量', example: 10 })
@Rule(RuleType.number()) @Rule(RuleType.number().default(10))
pageSize: number; pageSize: number;
@ApiProperty({ example: 'ZYN', description: '关键字' }) @ApiProperty({ description: '搜索关键字', required: false })
@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 })
@Rule(RuleType.string()) @Rule(RuleType.string())
name?: 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 { export class SetProductComponentsDTO {
@ApiProperty({ description: '组成项列表', type: [ProductComponentItemDTO] }) @ApiProperty({ description: '产品组成', type: 'array', required: true })
@Rule(RuleType.array().items(RuleType.object())) @Rule(
items: ProductComponentItemDTO[]; RuleType.array()
.items(
RuleType.object({
sku: RuleType.string().required(),
quantity: RuleType.number().required(),
})
)
.required()
)
components: { sku: string; quantity: number }[];
} }

View File

@ -44,6 +44,11 @@ export class CreateSiteDTO {
type?: string; type?: string;
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
skuPrefix?: string; skuPrefix?: string;
// 区域
@ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[];
} }
export class UpdateSiteDTO { export class UpdateSiteDTO {
@ -61,6 +66,11 @@ export class UpdateSiteDTO {
type?: string; type?: string;
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
skuPrefix?: string; skuPrefix?: string;
// 区域
@ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[];
} }
export class QuerySiteDTO { export class QuerySiteDTO {

View File

@ -167,6 +167,11 @@ export class CreateStockPointDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
contactPhone: string; contactPhone: string;
// 区域
@ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[];
} }
export class UpdateStockPointDTO extends CreateStockPointDTO {} export class UpdateStockPointDTO extends CreateStockPointDTO {}

View File

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

View File

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

View File

@ -7,10 +7,13 @@ import {
ManyToMany, ManyToMany,
JoinTable, JoinTable,
OneToMany, OneToMany,
ManyToOne,
JoinColumn,
} from 'typeorm'; } from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { DictItem } from './dict_item.entity'; import { DictItem } from './dict_item.entity';
import { ProductStockComponent } from './product_stock_component.entity'; import { ProductStockComponent } from './product_stock_component.entity';
import { Category } from './category.entity';
@Entity() @Entity()
export class Product { export class Product {
@ -64,6 +67,12 @@ export class Product {
@Column({ default: 0 }) @Column({ default: 0 })
stock: number; stock: number;
// 分类关联
@ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'categoryId' })
category: Category;
@ManyToMany(() => DictItem, dictItem => dictItem.products, { @ManyToMany(() => DictItem, dictItem => dictItem.products, {
cascade: true, cascade: true,
}) })

View File

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

View File

@ -4,16 +4,8 @@ import { Product } from '../entity/product.entity';
import { paginate } from '../utils/paginate.util'; import { paginate } from '../utils/paginate.util';
import { PaginationParams } from '../interface'; import { PaginationParams } from '../interface';
import { import {
CreateBrandDTO,
CreateFlavorsDTO,
CreateProductDTO, CreateProductDTO,
CreateStrengthDTO,
CreateSizeDTO,
UpdateBrandDTO,
UpdateFlavorsDTO,
UpdateProductDTO, UpdateProductDTO,
UpdateStrengthDTO,
UpdateSizeDTO,
} from '../dto/product.dto'; } from '../dto/product.dto';
import { import {
BrandPaginatedResponse, BrandPaginatedResponse,
@ -33,6 +25,7 @@ import { StockService } from './stock.service';
import { Stock } from '../entity/stock.entity'; import { Stock } from '../entity/stock.entity';
import { StockPoint } from '../entity/stock_point.entity'; import { StockPoint } from '../entity/stock_point.entity';
import { ProductStockComponent } from '../entity/product_stock_component.entity'; import { ProductStockComponent } from '../entity/product_stock_component.entity';
import { Category } from '../entity/category.entity';
@Provide() @Provide()
export class ProductService { export class ProductService {
@ -69,6 +62,17 @@ export class ProductService {
@InjectEntityModel(ProductStockComponent) @InjectEntityModel(ProductStockComponent)
productStockComponentModel: Repository<ProductStockComponent>; productStockComponentModel: Repository<ProductStockComponent>;
@InjectEntityModel(Category)
categoryModel: Repository<Category>;
// 获取所有 WordPress 商品
async getWpProducts() {
return this.wpProductModel.find();
}
// async findProductsByName(name: string): Promise<Product[]> { // async findProductsByName(name: string): Promise<Product[]> {
// const where: any = {}; // const where: any = {};
// const nameFilter = name ? name.split(' ').filter(Boolean) : []; // const nameFilter = name ? name.split(' ').filter(Boolean) : [];
@ -89,7 +93,8 @@ export class ProductService {
async findProductsByName(name: string): Promise<Product[]> { async findProductsByName(name: string): Promise<Product[]> {
const nameFilter = name ? name.split(' ').filter(Boolean) : []; const nameFilter = name ? name.split(' ').filter(Boolean) : [];
const query = this.productModel.createQueryBuilder('product'); const query = this.productModel.createQueryBuilder('product')
.leftJoinAndSelect('product.category', 'category');
// 保证 sku 不为空 // 保证 sku 不为空
query.where('product.sku IS NOT NULL'); query.where('product.sku IS NOT NULL');
@ -128,6 +133,7 @@ export class ProductService {
where: { where: {
sku, sku,
}, },
relations: ['category', 'attributes', 'attributes.dict']
}); });
} }
@ -139,7 +145,8 @@ export class ProductService {
const qb = this.productModel const qb = this.productModel
.createQueryBuilder('product') .createQueryBuilder('product')
.leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('product.attributes', 'attribute')
.leftJoinAndSelect('attribute.dict', 'dict'); .leftJoinAndSelect('attribute.dict', 'dict')
.leftJoinAndSelect('product.category', 'category');
// 模糊搜索 name支持多个关键词 // 模糊搜索 name支持多个关键词
const nameFilter = name ? name.split(' ').filter(Boolean) : []; const nameFilter = name ? name.split(' ').filter(Boolean) : [];
@ -231,16 +238,41 @@ export class ProductService {
} }
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> { 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) { if (!Array.isArray(attributes) || attributes.length === 0) {
throw new Error('属性列表不能为空'); // 如果提供了 categoryId 但没有 attributes初始化为空数组
if (!attributes && categoryId) {
// 继续执行,下面会处理 categoryId
} else {
throw new Error('属性列表不能为空');
}
} }
const safeAttributes = attributes || [];
// 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项) // 解析属性输入(中文注释:按 id 或 dictName 创建/关联字典项)
const resolvedAttributes: DictItem[] = []; 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; let item: DictItem | null = null;
if (attr.id) { if (attr.id) {
// 中文注释:如果传入了 id直接查找字典项并使用不强制要求 dictName // 中文注释:如果传入了 id直接查找字典项并使用不强制要求 dictName
@ -274,6 +306,9 @@ export class ProductService {
product.name = name; product.name = name;
product.description = description; product.description = description;
product.attributes = resolvedAttributes; product.attributes = resolvedAttributes;
if (categoryItem) {
product.category = categoryItem;
}
// 条件判断(中文注释:设置商品类型,默认 simple // 条件判断(中文注释:设置商品类型,默认 simple
product.type = (createProductDTO.type as any) || 'single'; product.type = (createProductDTO.type as any) || 'single';
@ -310,7 +345,7 @@ export class ProductService {
updateProductDTO: UpdateProductDTO updateProductDTO: UpdateProductDTO
): Promise<Product> { ): 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) { if (!product) {
throw new Error(`产品 ID ${id} 不存在`); throw new Error(`产品 ID ${id} 不存在`);
} }
@ -322,6 +357,16 @@ export class ProductService {
if (updateProductDTO.description !== undefined) { if (updateProductDTO.description !== undefined) {
product.description = updateProductDTO.description; 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) { if (updateProductDTO.price !== undefined) {
product.price = Number(updateProductDTO.price); product.price = Number(updateProductDTO.price);
} }
@ -350,6 +395,15 @@ export class ProductService {
}; };
for (const attr of updateProductDTO.attributes) { 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; let item: DictItem | null = null;
if (attr.id) { if (attr.id) {
// 中文注释:当提供 id 时直接查询字典项,不强制要求 dictName // 中文注释:当提供 id 时直接查询字典项,不强制要求 dictName
@ -629,7 +683,7 @@ export class ProductService {
return this.dictItemModel.find({ where: { dict: { id: brandDict.id } } }); 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; const { title, name } = createBrandDTO;
// 查找 'brand' 字典 // 查找 'brand' 字典
@ -652,7 +706,7 @@ export class ProductService {
return await this.dictItemModel.save(brand); 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 }); const brand = await this.dictItemModel.findOneBy({ id });
if (!brand) { if (!brand) {
@ -712,7 +766,7 @@ export class ProductService {
return this.dictItemModel.find({ where: { dict: { id: flavorsDict.id } } }); 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 { title, name } = createFlavorsDTO;
const flavorsDict = await this.dictModel.findOne({ const flavorsDict = await this.dictModel.findOne({
where: { name: 'flavor' }, where: { name: 'flavor' },
@ -727,7 +781,7 @@ export class ProductService {
return await this.dictItemModel.save(flavors); 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 }); const flavors = await this.dictItemModel.findOneBy({ id });
if (!flavors) { if (!flavors) {
throw new Error(`口味 ID ${id} 不存在`); throw new Error(`口味 ID ${id} 不存在`);
@ -779,7 +833,7 @@ export class ProductService {
return this.dictItemModel.find({ where: { dict: { id: sizeDict.id } } }) as any; 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; const { title, name } = createSizeDTO;
// 获取 size 字典(中文注释:用于挂载尺寸项) // 获取 size 字典(中文注释:用于挂载尺寸项)
const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } }); const sizeDict = await this.dictModel.findOne({ where: { name: 'size' } });
@ -795,7 +849,7 @@ export class ProductService {
return await this.dictItemModel.save(size); 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 }); const size = await this.dictItemModel.findOneBy({ id });
// 条件判断(中文注释:不存在则报错) // 条件判断(中文注释:不存在则报错)
@ -866,7 +920,7 @@ export class ProductService {
return this.dictItemModel.find({ where: { dict: { id: strengthDict.id } } }); 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 { title, name } = createStrengthDTO;
const strengthDict = await this.dictModel.findOne({ const strengthDict = await this.dictModel.findOne({
where: { name: 'strength' }, where: { name: 'strength' },
@ -950,7 +1004,7 @@ export class ProductService {
await this.dictItemModel.delete({ id }); await this.dictItemModel.delete({ id });
} }
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) { async updateStrength(id: number, updateStrength: any) {
const strength = await this.dictItemModel.findOneBy({ id }); const strength = await this.dictItemModel.findOneBy({ id });
if (!strength) { if (!strength) {
throw new Error(`规格 ID ${id} 不存在`); throw new Error(`规格 ID ${id} 不存在`);

View File

@ -3,7 +3,8 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository, Like, In } from 'typeorm'; import { Repository, Like, In } from 'typeorm';
import { Site } from '../entity/site.entity'; import { Site } from '../entity/site.entity';
import { WpSite } from '../interface'; import { WpSite } from '../interface';
import { UpdateSiteDTO } from '../dto/site.dto'; import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
import { Area } from '../entity/area.entity';
@Provide() @Provide()
@Scope(ScopeEnum.Singleton) @Scope(ScopeEnum.Singleton)
@ -11,11 +12,16 @@ export class SiteService {
@InjectEntityModel(Site) @InjectEntityModel(Site)
siteModel: Repository<Site>; siteModel: Repository<Site>;
@InjectEntityModel(Area)
areaModel: Repository<Area>;
async syncFromConfig(sites: WpSite[] = []) { async syncFromConfig(sites: WpSite[] = []) {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
for (const siteConfig of sites) { 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 实体字段 // 将 WpSite 字段映射为 Site 实体字段
const payload: Partial<Site> = { const payload: Partial<Site> = {
name: siteConfig.name, name: siteConfig.name,
@ -25,66 +31,145 @@ export class SiteService {
type: 'woocommerce', type: 'woocommerce',
}; };
// 存在则更新,不存在则插入新记录 // 存在则更新,不存在则插入新记录
if (exist) await this.siteModel.update({ id: exist.id }, payload); if (exist) {
else await this.siteModel.insert(payload as Site); await this.siteModel.update({ id: exist.id }, payload);
} else {
await this.siteModel.insert(payload as Site);
}
} }
} }
async create(data: Partial<Site>) { async create(data: CreateSiteDTO) {
// 创建新的站点记录 // 从 DTO 中分离出区域代码和其他站点数据
await this.siteModel.insert(data as Site); 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; return true;
} }
async update(id: string | number, data: UpdateSiteDTO) { 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> = { const payload: Partial<Site> = {
...data, ...restData,
isDisabled: isDisabled:
data.isDisabled === undefined // 未传入则不更新该字段 data.isDisabled === undefined
? undefined ? undefined
: data.isDisabled // true -> 1, false -> 0 : data.isDisabled
? 1 ? 1
: 0, : 0,
} as any; } 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; return true;
} }
async get(id: string | number, includeSecret = false) { async get(id: string | number, includeSecret = false) {
// 根据主键获取站点includeSecret 为 true 时返回密钥字段 // 根据主键获取站点,并使用 relations 加载关联的 areas
const site = await this.siteModel.findOne({ where: { id: Number(id) } }); const site = await this.siteModel.findOne({
if (!site) return null; where: { id: Number(id) },
if (includeSecret) return site; relations: ['areas'],
});
if (!site) {
return null;
}
// 如果需要包含密钥,则直接返回
if (includeSecret) {
return site;
}
// 默认不返回密钥,进行字段脱敏 // 默认不返回密钥,进行字段脱敏
const { consumerKey, consumerSecret, ...rest } = site; const { consumerKey, consumerSecret, ...rest } = site;
return rest; 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 列表过滤 // 分页查询站点列表,支持关键字、禁用状态与 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 = {}; 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) { if (ids) {
// 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值 // 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
const numIds = String(ids) const numIds = String(ids)
.split(',') .split(',')
.filter(Boolean) .filter(Boolean)
.map((i) => Number(i)) .map(i => Number(i))
.filter((v) => !Number.isNaN(v)); .filter(v => !Number.isNaN(v));
if (numIds.length > 0) where.id = In(numIds); if (numIds.length > 0) {
where.id = In(numIds);
}
} }
// 进行分页查询skip/take并返回总条数 // 进行分页查询,并使用 relations 加载关联的 areas
const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize }); const [items, total] = await this.siteModel.findAndCount({
// 根据 includeSecret 决定是否脱敏返回密钥字段 where,
const data = includeSecret ? items : items.map((item: any) => { skip: (current - 1) * pageSize,
const { consumerKey, consumerSecret, ...rest } = item; take: pageSize,
return rest; relations: ['areas'],
}); });
// 根据 includeSecret 决定是否脱敏返回密钥字段
const data = includeSecret
? items
: items.map((item: any) => {
const { consumerKey, consumerSecret, ...rest } = item;
return rest;
});
return { items: data, total, current, pageSize }; return { items: data, total, current, pageSize };
} }

View File

@ -1,5 +1,5 @@
import { Provide } from '@midwayjs/core'; 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 { Stock } from '../entity/stock.entity';
import { StockRecord } from '../entity/stock_record.entity'; import { StockRecord } from '../entity/stock_record.entity';
import { paginate } from '../utils/paginate.util'; import { paginate } from '../utils/paginate.util';
@ -27,6 +27,7 @@ import { User } from '../entity/user.entity';
import dayjs = require('dayjs'); import dayjs = require('dayjs');
import { Transfer } from '../entity/transfer.entity'; import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity'; import { TransferItem } from '../entity/transfer_item.entity';
import { Area } from '../entity/area.entity';
@Provide() @Provide()
export class StockService { export class StockService {
@ -51,35 +52,55 @@ export class StockService {
@InjectEntityModel(TransferItem) @InjectEntityModel(TransferItem)
transferItemModel: Repository<TransferItem>; transferItemModel: Repository<TransferItem>;
@InjectEntityModel(Area)
areaModel: Repository<Area>;
async createStockPoint(data: CreateStockPointDTO) { async createStockPoint(data: CreateStockPointDTO) {
const { name, location, contactPerson, contactPhone } = data; const { areas: areaCodes, ...restData } = data;
const stockPoint = new StockPoint(); const stockPoint = new StockPoint();
stockPoint.name = name; Object.assign(stockPoint, restData);
stockPoint.location = location;
stockPoint.contactPerson = contactPerson; if (areaCodes && areaCodes.length > 0) {
stockPoint.contactPhone = contactPhone; const areas = await this.areaModel.findBy({ code: In(areaCodes) });
stockPoint.areas = areas;
} else {
stockPoint.areas = [];
}
await this.stockPointModel.save(stockPoint); await this.stockPointModel.save(stockPoint);
} }
async updateStockPoint(id: number, data: UpdateStockPointDTO) { async updateStockPoint(id: number, data: UpdateStockPointDTO) {
// 确认产品是否存在 const { areas: areaCodes, ...restData } = data;
const point = await this.stockPointModel.findOneBy({ id }); const pointToUpdate = await this.stockPointModel.findOneBy({ id });
if (!point) { if (!pointToUpdate) {
throw new Error(`产品 ID ${id} 不存在`); 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) { async getStockPoints(query: QueryPointDTO) {
const { current = 1, pageSize = 10 } = query; const { current = 1, pageSize = 10 } = query;
return await paginate(this.stockPointModel, { return await paginate(this.stockPointModel, {
pagination: { current, pageSize }, pagination: { current, pageSize },
relations: ['areas'],
}); });
} }
async getAllStockPoints(): Promise<StockPoint[]> { async getAllStockPoints(): Promise<StockPoint[]> {
return await this.stockPointModel.find(); return await this.stockPointModel.find({ relations: ['areas'] });
} }
async delStockPoints(id: number) { async delStockPoints(id: number) {

35
test_db_count.ts Normal file
View File

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