Compare commits

..

2 Commits

Author SHA1 Message Date
tikkhun cd72783291 feat: 添加区域管理功能及相关实体关系
- 新增Area实体及相关DTO、Service、Controller
- 在Site和StockPoint实体中添加与Area的多对多关系
- 添加区域数据迁移文件和种子数据
- 修改Site实体字段siteName为name并保持兼容
- 在Product实体中添加promotionPrice和source字段
- 添加模板渲染接口
- 完善模板删除前的校验逻辑
2025-11-28 10:13:21 +08:00
tikkhun 46cfaa24e7 feat: 添加商品价格字段并优化模板分页查询
- 在product和wp_product实体中添加价格字段
- 为模板服务添加分页查询功能
- 更新模板控制器以支持分页参数
- 将Template实体添加到数据源
- 修改QueryBrandDTO的name字段为非必填
2025-11-28 00:19:52 +08:00
18 changed files with 442 additions and 25 deletions

View File

@ -34,6 +34,7 @@ import { Site } from '../entity/site.entity';
import { Dict } from '../entity/dict.entity';
import { DictItem } from '../entity/dict_item.entity';
import { Template } from '../entity/template.entity';
import { Area } from '../entity/area.entity';
import DictSeeder from '../db/seeds/dict.seeder';
export default {
@ -76,7 +77,8 @@ export default {
Site,
Dict,
DictItem,
Template
Template,
Area,
],
synchronize: true,
logging: false,

View File

@ -0,0 +1,110 @@
import { Inject } from '@midwayjs/core';
import {
Body,
Controller,
Del,
Get,
Param,
Post,
Put,
Query,
} from '@midwayjs/decorator';
import {
ApiBearerAuth,
ApiBody,
ApiExtension,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@midwayjs/swagger';
import { AreaService } from '../service/area.service';
import { CreateAreaDTO, QueryAreaDTO, UpdateAreaDTO } from '../dto/area.dto';
import { errorResponse, successResponse } from '../utils/response.util';
import { Area } from '../entity/area.entity';
@ApiBearerAuth()
@ApiTags('Area')
@Controller('/api/area')
export class AreaController {
@Inject()
areaService: AreaService;
@ApiOperation({ summary: '创建区域' })
@ApiBody({ type: CreateAreaDTO })
@ApiOkResponse({ type: Area, description: '成功创建的区域' })
@Post('/')
async createArea(@Body() area: CreateAreaDTO) {
try {
const newArea = await this.areaService.createArea(area);
return successResponse(newArea, '创建成功');
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOperation({ summary: '更新区域' })
@ApiBody({ type: UpdateAreaDTO })
@ApiOkResponse({ type: Area, description: '成功更新的区域' })
@Put('/:id')
async updateArea(@Param('id') id: number, @Body() area: UpdateAreaDTO) {
try {
const updatedArea = await this.areaService.updateArea(id, area);
return successResponse(updatedArea, '更新成功');
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOperation({ summary: '删除区域' })
@ApiOkResponse({ description: '删除成功' })
@Del('/:id')
async deleteArea(@Param('id') id: number) {
try {
await this.areaService.deleteArea(id);
return successResponse(null, '删除成功');
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOperation({ summary: '获取区域列表(分页)' })
@ApiOkResponse({ type: [Area], description: '区域列表' })
@ApiExtension('x-pagination', { currentPage: 1, pageSize: 10, total: 100 })
@Get('/')
async getAreaList(@Query() query: QueryAreaDTO) {
try {
const { list, total } = await this.areaService.getAreaList(query);
return successResponse({ list, total }, '查询成功');
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOperation({ summary: '获取所有区域' })
@ApiOkResponse({ type: [Area], description: '所有区域列表' })
@Get('/all')
async getAllAreas() {
try {
const areas = await this.areaService.getAllAreas();
return successResponse(areas, '查询成功');
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOperation({ summary: '根据ID获取区域详情' })
@ApiOkResponse({ type: Area, description: '区域详情' })
@Get('/:id')
async getAreaById(@Param('id') id: number) {
try {
const area = await this.areaService.getAreaById(id);
if (!area) {
return errorResponse('区域不存在');
}
return successResponse(area, '查询成功');
} catch (error) {
return errorResponse(error.message);
}
}
}

View File

@ -1,4 +1,4 @@
import { Inject, Controller, Get, Post, Put, Del, Body, Param } from '@midwayjs/core';
import { Inject, Controller, Get, Post, Put, Del, Body, Param, Query } from '@midwayjs/core';
import { TemplateService } from '../service/template.service';
import { successResponse, errorResponse } from '../utils/response.util';
import { CreateTemplateDTO, UpdateTemplateDTO } from '../dto/template.dto';
@ -20,17 +20,10 @@ export class TemplateController {
* @description
*/
@ApiOkResponse({ type: [Template], description: '成功获取模板列表' })
@Get('/')
async getTemplateList() {
try {
// 调用服务层获取列表
const data = await this.templateService.getTemplateList();
// 返回成功响应
return successResponse(data);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
@Get('/list')
async getTemplateList(@Query() params: any) {
// 调用服务层获取列表
return this.templateService.getTemplateList(params);
}
/**
@ -112,4 +105,27 @@ export class TemplateController {
return errorResponse(error.message);
}
}
/**
* @summary
* @description
* @param name
* @param data
*/
@ApiOkResponse({ type: String, description: '成功渲染模板' })
@Post('/render/:name')
async renderTemplate(
@Param('name') name: string,
@Body() data: Record<string, any>
) {
try {
// 调用服务层渲染模板
const renderedString = await this.templateService.render(name, data);
// 返回成功响应
return successResponse(renderedString);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
}

View File

@ -34,6 +34,8 @@ import { Subscription } from '../entity/subscription.entity';
import { Site } from '../entity/site.entity';
import { Dict } from '../entity/dict.entity';
import { DictItem } from '../entity/dict_item.entity';
import { Template } from '../entity/template.entity';
import { Area } from '../entity/area.entity';
const options: DataSourceOptions & SeederOptions = {
type: 'mysql',
@ -79,8 +81,10 @@ const options: DataSourceOptions & SeederOptions = {
Site,
Dict,
DictItem,
Template,
Area,
],
migrations: ['src/migration/*.ts'],
migrations: ['src/db/migrations/**/*.ts'],
seeds: ['src/db/seeds/**/*.ts'],
};

View File

@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Area1764294088896 implements MigrationInterface {
name = 'Area1764294088896'
public async up(queryRunner: QueryRunner): Promise<void> {
// await queryRunner.query(`DROP INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\``);
// await queryRunner.query(`CREATE TABLE \`area\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_644ffaf8fbde4db798cb47712f\` (\`name\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
// await queryRunner.query(`CREATE TABLE \`stock_point_areas_area\` (\`stockPointId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_07d2db2150151e2ef341d2f1de\` (\`stockPointId\`), INDEX \`IDX_92707ea81fc19dc707dba24819\` (\`areaId\`), PRIMARY KEY (\`stockPointId\`, \`areaId\`)) ENGINE=InnoDB`);
// await queryRunner.query(`CREATE TABLE \`site_areas_area\` (\`siteId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_926a14ac4c91f38792831acd2a\` (\`siteId\`), INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` (\`areaId\`), PRIMARY KEY (\`siteId\`, \`areaId\`)) ENGINE=InnoDB`);
// await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`siteName\``);
// await queryRunner.query(`ALTER TABLE `product` ADD `promotionPrice` decimal(10,2) NOT NULL DEFAULT '0.00'`);
// await queryRunner.query(`ALTER TABLE `product` ADD `source` int NOT NULL DEFAULT '0'`);
// await queryRunner.query(`ALTER TABLE \`site\` ADD \`token\` varchar(255) NULL`);
// await queryRunner.query(`ALTER TABLE `site` ADD `name` varchar(255) NOT NULL`);
// await queryRunner.query(`ALTER TABLE \`site\` ADD UNIQUE INDEX \`IDX_9669a09fcc0eb6d2794a658f64\` (\`name\`)`);
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` ADD CONSTRAINT \`FK_07d2db2150151e2ef341d2f1de1\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` ADD CONSTRAINT \`FK_92707ea81fc19dc707dba24819c\` FOREIGN KEY (\`areaId\`) REFERENCES \`area\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE \`site_areas_area\` ADD CONSTRAINT \`FK_926a14ac4c91f38792831acd2a6\` FOREIGN KEY (\`siteId\`) REFERENCES \`site\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE \`site_areas_area\` ADD CONSTRAINT \`FK_7c26c582048e3ecd3cd5938cb9f\` FOREIGN KEY (\`areaId\`) REFERENCES \`area\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`site_areas_area\` DROP FOREIGN KEY \`FK_7c26c582048e3ecd3cd5938cb9f\``);
await queryRunner.query(`ALTER TABLE \`site_areas_area\` DROP FOREIGN KEY \`FK_926a14ac4c91f38792831acd2a6\``);
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` DROP FOREIGN KEY \`FK_92707ea81fc19dc707dba24819c\``);
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` DROP FOREIGN KEY \`FK_07d2db2150151e2ef341d2f1de1\``);
await queryRunner.query(`ALTER TABLE \`site\` DROP INDEX \`IDX_9669a09fcc0eb6d2794a658f64\``);
await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`name\``);
await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`token\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`source\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`promotionPrice\``);
await queryRunner.query(`ALTER TABLE \`site\` ADD \`siteName\` varchar(255) NOT NULL`);
await queryRunner.query(`DROP INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` ON \`site_areas_area\``);
await queryRunner.query(`DROP INDEX \`IDX_926a14ac4c91f38792831acd2a\` ON \`site_areas_area\``);
await queryRunner.query(`DROP TABLE \`site_areas_area\``);
await queryRunner.query(`DROP INDEX \`IDX_92707ea81fc19dc707dba24819\` ON \`stock_point_areas_area\``);
await queryRunner.query(`DROP INDEX \`IDX_07d2db2150151e2ef341d2f1de\` ON \`stock_point_areas_area\``);
await queryRunner.query(`DROP TABLE \`stock_point_areas_area\``);
await queryRunner.query(`DROP INDEX \`IDX_644ffaf8fbde4db798cb47712f\` ON \`area\``);
await queryRunner.query(`DROP TABLE \`area\``);
await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\` (\`siteName\`)`);
}
}

View File

@ -0,0 +1,26 @@
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { Area } from '../../entity/area.entity';
export default class AreaSeeder implements Seeder {
public async run(
dataSource: DataSource,
factoryManager: SeederFactoryManager
): Promise<any> {
const repository = dataSource.getRepository(Area);
const areas = [
{ name: '加拿大' },
{ name: '澳大利亚' },
{ name: '欧洲' },
];
for (const area of areas) {
const existing = await repository.findOne({ where: { name: area.name } });
if (!existing) {
await repository.insert(area);
}
}
}
}

32
src/dto/area.dto.ts Normal file
View File

@ -0,0 +1,32 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
// 创建区域的数据传输对象
export class CreateAreaDTO {
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
@Rule(RuleType.string().required())
name: string;
}
// 更新区域的数据传输对象
export class UpdateAreaDTO {
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
@Rule(RuleType.string())
name?: string;
}
// 查询区域的数据传输对象
export class QueryAreaDTO {
@ApiProperty({ type: 'number', description: '当前页码', example: 1 })
@Rule(RuleType.number().min(1).default(1))
currentPage: number;
@ApiProperty({ type: 'number', description: '每页数量', example: 10 })
@Rule(RuleType.number().min(1).max(100).default(10))
pageSize: number;
@ApiProperty({ type: 'string', description: '区域名称', example: '欧洲' })
@Rule(RuleType.string())
name?: string;
}

View File

@ -61,6 +61,11 @@ export class CreateProductDTO {
@ApiProperty()
@Rule(RuleType.string())
humidity: string;
// 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number())
price?: number;
}
/**
@ -132,7 +137,7 @@ export class QueryBrandDTO {
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string().required())
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}

35
src/entity/area.entity.ts Normal file
View File

@ -0,0 +1,35 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('area')
export class Area {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: 'string', description: '区域名称' })
@Column({ unique: true })
name: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -42,12 +42,27 @@ export class Product {
@Column({ unique: true })
sku: string;
// 商品价格
@ApiProperty({ description: '价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
price: number;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number;
@ManyToMany(() => DictItem, {
cascade: true,
})
@JoinTable()
attributes: DictItem[];
// 来源
@ApiProperty({ description: '来源', example: '1' })
@Column({ default: 0 })
source: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',

View File

@ -1,4 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Area } from './area.entity';
@Entity('site')
export class Site {
@ -14,8 +15,11 @@ export class Site {
@Column({ length: 255, nullable: true })
consumerSecret: string;
@Column({ nullable: true })
token: string;
@Column({ length: 255, unique: true })
siteName: string;
name: string;
@Column({ length: 32, default: 'woocommerce' })
type: string; // 平台类型woocommerce | shopyy
@ -25,4 +29,8 @@ export class Site {
@Column({ default: false })
isDisabled: boolean;
@ManyToMany(() => Area)
@JoinTable()
areas: Area[];
}

View File

@ -8,8 +8,11 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
OneToMany,
ManyToMany,
JoinTable,
} from 'typeorm';
import { Shipment } from './shipment.entity';
import { Area } from './area.entity';
@Entity('stock_point')
export class StockPoint extends BaseEntity {
@ -72,4 +75,8 @@ export class StockPoint extends BaseEntity {
@DeleteDateColumn()
deletedAt: Date; // 软删除时间
@ManyToMany(() => Area)
@JoinTable()
areas: Area[];
}

View File

@ -24,7 +24,14 @@ export class Template {
@ApiProperty({ nullable: true ,name:"描述"})
@Column('text',{nullable: true,comment: "描述"})
description?: string;
@ApiProperty({
example: true,
description: '是否可删除',
required: true,
})
@Column({ default: true })
deletable: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',

View File

@ -39,7 +39,7 @@ export class WpProduct {
@Column()
externalProductId: string;
@ApiProperty({ description: 'sku', type: 'string' })
@ApiProperty({ description: '商店sku', type: 'string' })
@Column({ nullable: true })
sku?: string;

View File

@ -0,0 +1,90 @@
import { Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { Area } from '../entity/area.entity';
import { CreateAreaDTO, QueryAreaDTO, UpdateAreaDTO } from '../dto/area.dto';
@Provide()
export class AreaService {
@InjectEntityModel(Area)
areaModel: Repository<Area>;
/**
*
* @param params
*/
async createArea(params: CreateAreaDTO) {
// 检查区域名称是否已存在
const existing = await this.areaModel.findOne({ where: { name: params.name } });
if (existing) {
throw new Error('区域名称已存在');
}
const area = new Area();
area.name = params.name;
return await this.areaModel.save(area);
}
/**
*
* @param id ID
* @param params
*/
async updateArea(id: number, params: UpdateAreaDTO) {
const area = await this.areaModel.findOneBy({ id });
if (!area) {
throw new Error('区域不存在');
}
if (params.name) {
// 检查新的区域名称是否已存在
const existing = await this.areaModel.findOne({ where: { name: params.name } });
if (existing && existing.id !== id) {
throw new Error('区域名称已存在');
}
area.name = params.name;
}
return await this.areaModel.save(area);
}
/**
*
* @param id ID
*/
async deleteArea(id: number) {
const result = await this.areaModel.delete(id);
return result.affected > 0;
}
/**
*
* @param query
*/
async getAreaList(query: QueryAreaDTO) {
const { currentPage, pageSize, name } = query;
const [list, total] = await this.areaModel.findAndCount({
where: name ? { name } : {},
skip: (currentPage - 1) * pageSize,
take: pageSize,
order: {
id: 'DESC',
},
});
return { list, total };
}
/**
*
*/
async getAllAreas() {
return await this.areaModel.find();
}
/**
* ID获取区域详情
* @param id ID
*/
async getAreaById(id: number) {
return await this.areaModel.findOneBy({ id });
}
}

View File

@ -1350,7 +1350,7 @@ export class OrderService {
return {
...order,
siteName: site?.siteName,
siteName: site?.name,
// Site 实体无邮箱字段,这里返回空字符串保持兼容
email: '',
items,

View File

@ -15,10 +15,10 @@ export class SiteService {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
for (const siteConfig of sites) {
// 按站点名称查询是否已存在记录
const exist = await this.siteModel.findOne({ where: { siteName: siteConfig.siteName } });
const exist = await this.siteModel.findOne({ where: { name: siteConfig.siteName } });
// 将 WpSite 字段映射为 Site 实体字段
const payload: Partial<Site> = {
siteName: siteConfig.siteName,
name: siteConfig.siteName,
apiUrl: (siteConfig as any).wpApiUrl,
consumerKey: (siteConfig as any).consumerKey,
consumerSecret: (siteConfig as any).consumerSecret,

View File

@ -17,9 +17,14 @@ export class TemplateService {
*
* @returns {Promise<Template[]>}
*/
async getTemplateList(): Promise<Template[]> {
// 使用 find 方法查询所有模板
return this.templateModel.find();
async getTemplateList(params: { currentPage?: number, pageSize?: number } = {}): Promise<{ items: Template[], total: number }> {
const { currentPage = 1, pageSize = 10 } = params;
// 使用 findAndCount 方法查询所有模板
const [items, total] = await this.templateModel.findAndCount({
skip: (currentPage - 1) * pageSize,
take: pageSize,
});
return { items, total };
}
/**
@ -76,6 +81,16 @@ export class TemplateService {
* @returns {Promise<boolean>} true false
*/
async deleteTemplate(id: number): Promise<boolean> {
// 首先根据 ID 查找模板
const template = await this.templateModel.findOneBy({ id });
// 如果模板不存在,则抛出错误
if (!template) {
throw new Error(`模板 ID ${id} 不存在`);
}
// 如果模板不可删除,则抛出错误
if (!template.deletable) {
throw new Error(`模板 ${template.name} 不可删除`);
}
// 执行删除操作
const result = await this.templateModel.delete(id);
// 如果影响的行数大于 0则表示删除成功