diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 423dc20..f826242 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -41,6 +41,7 @@ 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'; +import { SiteSku } from '../entity/site-sku.entity'; export default { // use for cookie sign key, should change to your own and keep security @@ -50,6 +51,7 @@ export default { entities: [ Product, ProductStockComponent, + SiteSku, User, PurchaseOrder, PurchaseOrderItem, diff --git a/src/db/migrations/migrate-site-skus.ts b/src/db/migrations/migrate-site-skus.ts new file mode 100644 index 0000000..f232d04 --- /dev/null +++ b/src/db/migrations/migrate-site-skus.ts @@ -0,0 +1,77 @@ +import { Bootstrap } from '@midwayjs/bootstrap'; +import { Product } from '../../entity/product.entity'; +import { SiteSku } from '../../entity/site-sku.entity'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { Provide } from '@midwayjs/core'; + +@Provide() +export class MigrateSiteSkus { + @InjectEntityModel(Product) + productModel: Repository; + + @InjectEntityModel(SiteSku) + siteSkuModel: Repository; + + async main() { + console.log('开始迁移 siteSkus 数据...'); + + try { + // 获取所有产品 + const products = await this.productModel.find(); + + console.log(`找到 ${products.length} 个产品需要检查 siteSkus 数据`); + + let migratedCount = 0; + + for (const product of products) { + // 检查 siteSkus 是否为字符串数组 + if (Array.isArray(product.siteSkus) && product.siteSkus.length > 0) { + for (const siteSku of product.siteSkus) { + // 检查是否已存在该 SKU + const existingSiteSku = await this.siteSkuModel.findOne({ + where: { sku: siteSku.sku }, + }); + + if (!existingSiteSku) { + // 创建新的 SiteSku 实体 + const siteSku = new SiteSku(); + siteSku.sku = siteSku.sku; + siteSku.productId = product.id; + siteSku.isOld = true; + + await this.siteSkuModel.save(siteSku); + migratedCount++; + } else if (!existingSiteSku.productId) { + // 如果已存在但未关联产品,则关联 + existingSiteSku.productId = product.id; + existingSiteSku.isOld = true; + await this.siteSkuModel.save(existingSiteSku); + migratedCount++; + } + } + } + } + + console.log(`成功迁移 ${migratedCount} 条 siteSku 数据`); + console.log('数据迁移完成!'); + } catch (error) { + console.error('数据迁移失败:', error); + process.exit(1); + } + } +} + +// 运行迁移 +if (require.main === module) { + Bootstrap + .run() + .then(async (app) => { + const migrateService = app.get(MigrateSiteSkus); + await migrateService.main(); + }) + .catch(error => { + console.error('启动失败:', error); + process.exit(1); + }); +} diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index d1c52e7..18aad25 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Rule, RuleType } from '@midwayjs/validate'; import { UnifiedSearchParamsDTO } from './api.dto'; +import { SiteSku } from '../entity/site-sku.entity'; /** * 属性输入DTO @@ -65,7 +66,7 @@ export class CreateProductDTO { @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @Rule(RuleType.array().items(RuleType.string()).optional()) - siteSkus?: string[]; + siteSkus?: SiteSku['sku'][]; // 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等) // 当 type 为 'single' 时必填,当 type 为 'bundle' 时可选 @@ -152,7 +153,7 @@ export class UpdateProductDTO { @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @Rule(RuleType.array().items(RuleType.string()).optional()) - siteSkus?: string[]; + siteSkus?: SiteSku['sku'][]; // 商品价格 @ApiProperty({ description: '价格', example: 99.99, required: false }) @@ -197,7 +198,6 @@ export class UpdateProductDTO { components?: { sku: string; quantity: number }[]; } - /** * DTO 用于批量更新产品属性 */ @@ -232,7 +232,7 @@ export class BatchUpdateProductDTO { @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @Rule(RuleType.array().items(RuleType.string()).optional()) - siteSkus?: string[]; + siteSkus?: SiteSku['sku'][]; @ApiProperty({ description: '价格', example: 99.99, required: false }) @Rule(RuleType.number().optional()) diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index 11cf00d..1a1bf22 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -14,6 +14,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { DictItem } from './dict_item.entity'; import { ProductStockComponent } from './product_stock_component.entity'; import { Category } from './category.entity'; +import { SiteSku } from './site-sku.entity'; @Entity('product') export class Product { @@ -98,9 +99,11 @@ export class Product { @OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true }) components: ProductStockComponent[]; - @ApiProperty({ description: '站点 SKU 列表', type: 'string', isArray: true }) - @Column({ type: 'simple-array' ,nullable:true}) - siteSkus: string[]; + + // 站点 SKU 关联 + @ApiProperty({ description: '站点 SKU关联', type: SiteSku, isArray: true }) + @OneToMany(() => SiteSku, siteSku => siteSku.product, { cascade: true }) + siteSkus: SiteSku[]; // 来源 @ApiProperty({ description: '来源', example: '1' }) diff --git a/src/entity/product_stock_component.entity.ts b/src/entity/product_stock_component.entity.ts index e076a4f..80a079d 100644 --- a/src/entity/product_stock_component.entity.ts +++ b/src/entity/product_stock_component.entity.ts @@ -1,7 +1,9 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { Product } from './product.entity'; - +// 套餐产品构成 item(sku,0013,xx20,mixed008)n--siteSkus(0013,xx20,xx)旧版数据-->product-component(0020)(套装(1 5 10 20 50 100 (erp 手动创建)mixed))->(product(单品)+ quantity)->sales +// CA -> site-product -> components +// PRODUCT->compoentn(一次性做完) product+关联sku @Entity('product_stock_component') export class ProductStockComponent { @ApiProperty({ type: Number }) @@ -11,15 +13,15 @@ export class ProductStockComponent { @ApiProperty({ type: Number }) @Column() productId: number; - + // zi @ApiProperty({ description: '组件所关联的 SKU', type: 'string' }) @Column({ type: 'varchar', length: 64 }) sku: string; - + // zi @ApiProperty({ type: Number, description: '组成数量' }) @Column({ type: 'int', default: 1 }) quantity: number; - + // baba // 多对一,组件隶属于一个产品 @ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' }) product: Product; diff --git a/src/entity/site-product.entity.ts b/src/entity/site-product.entity.ts index fa6723f..7b338b9 100644 --- a/src/entity/site-product.entity.ts +++ b/src/entity/site-product.entity.ts @@ -34,7 +34,7 @@ export class SiteProduct { sku: string; @ApiProperty({ description: '类型' }) - @Column({ length: 16, default: 'single' }) + @Column({ length: 16, default: 'simple' }) type: string; @ApiProperty({ diff --git a/src/entity/site-sku.entity.ts b/src/entity/site-sku.entity.ts new file mode 100644 index 0000000..03b4ba0 --- /dev/null +++ b/src/entity/site-sku.entity.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { ApiProperty } from '@midwayjs/swagger'; +import { Product } from './product.entity'; +@Entity('product_site_sku') +export class SiteSku { + @ApiProperty({ description: 'sku'}) + @Column({ primary: true }) + sku: string; + + @ApiProperty({ description: '商品ID' }) + @Column({ nullable: true }) + productId: number; + // 商品关联 + @ManyToOne(() => Product, product => product.siteSkus) + @JoinColumn({ name: 'productId' }) + product: Product; + + @ApiProperty({ description: '是否旧版数据' }) + @Column({ default: false }) + isOld: boolean; +} \ No newline at end of file diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 5cde38a..9c55bb3 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -2176,6 +2176,7 @@ export class OrderService { * @param data 订单数据 * @returns 创建的订单 */ + // TODO 与 sync 逻辑不一致 如需要请修复。 async createOrder(data: Record) { // 从数据中解构出需要用的属性 const { siteId, sales, total, billing, customer_email, billing_phone } = data; diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 554e7bd..4198309 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1,12 +1,10 @@ import { Inject, Provide } from '@midwayjs/core'; -import * as fs from 'fs'; -import * as xlsx from 'xlsx'; -import { In, Like, Not, Repository } from 'typeorm'; -import { Product } from '../entity/product.entity'; -import { PaginationParams } from '../interface'; -import { paginate } from '../utils/paginate.util'; import { Context } from '@midwayjs/koa'; import { InjectEntityModel } from '@midwayjs/typeorm'; +import * as fs from 'fs'; +import { In, Like, Not, Repository } from 'typeorm'; +import * as xlsx from 'xlsx'; +import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchUpdateProductDTO, CreateProductDTO, @@ -20,20 +18,22 @@ import { SizePaginatedResponse, StrengthPaginatedResponse, } from '../dto/reponse.dto'; -import { Dict } from '../entity/dict.entity'; -import { DictItem } from '../entity/dict_item.entity'; -import { ProductStockComponent } from '../entity/product_stock_component.entity'; -import { Stock } from '../entity/stock.entity'; -import { StockPoint } from '../entity/stock_point.entity'; -import { StockService } from './stock.service'; -import { TemplateService } from './template.service'; - -import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { UnifiedProductDTO } from '../dto/site-api.dto'; import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto'; import { Category } from '../entity/category.entity'; import { CategoryAttribute } from '../entity/category_attribute.entity'; +import { Dict } from '../entity/dict.entity'; +import { DictItem } from '../entity/dict_item.entity'; +import { Product } from '../entity/product.entity'; +import { ProductStockComponent } from '../entity/product_stock_component.entity'; +import { SiteSku } from '../entity/site-sku.entity'; +import { Stock } from '../entity/stock.entity'; +import { StockPoint } from '../entity/stock_point.entity'; +import { PaginationParams } from '../interface'; +import { paginate } from '../utils/paginate.util'; import { SiteApiService } from './site-api.service'; +import { StockService } from './stock.service'; +import { TemplateService } from './template.service'; @Provide() export class ProductService { @@ -221,7 +221,7 @@ export class ProductService { } async findProductBySku(sku: string): Promise { - return this.productModel.findOne({ + return await this.productModel.findOne({ where: { sku, }, @@ -234,7 +234,8 @@ export class ProductService { .createQueryBuilder('product') .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict') - .leftJoinAndSelect('product.category', 'category'); + .leftJoinAndSelect('product.category', 'category') + .leftJoinAndSelect('product.siteSkus', 'siteSku'); // 处理分页参数(支持新旧两种格式) const page = query.page || 1; const pageSize = query.per_page || 10; @@ -474,7 +475,8 @@ export class ProductService { .createQueryBuilder('product') .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict') - .leftJoinAndSelect('product.category', 'category'); + .leftJoinAndSelect('product.category', 'category') + .leftJoinAndSelect('product.siteSkus', 'siteSkus'); // 验证分组字段 const groupBy = query.groupBy; @@ -870,7 +872,7 @@ export class ProductService { const product = new Product(); // 使用 merge 填充基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = createProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO; this.productModel.merge(product, simpleFields); product.attributes = resolvedAttributes; @@ -880,6 +882,16 @@ export class ProductService { // 确保默认类型 if (!product.type) product.type = 'single'; + // 处理站点 SKU + if (Array.isArray(_siteSkus)) { + product.siteSkus = _siteSkus.map(sku => { + const siteSku = new SiteSku(); + siteSku.sku = sku; + siteSku.isOld = false; + return siteSku; + }); + } + // 生成或设置 SKU(基于属性字典项的 name 生成) if (sku) { product.sku = sku; @@ -893,7 +905,7 @@ export class ProductService { if (createProductDTO.components && createProductDTO.components.length > 0) { await this.setProductComponents(savedProduct.id, createProductDTO.components); // 重新加载带组件的产品 - return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components', 'siteSkus'] }); + return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components'] }); } return savedProduct; @@ -910,7 +922,7 @@ export class ProductService { } // 使用 merge 更新基础字段,排除特殊处理字段 - const { attributes, categoryId, categoryName, sku, components, ...simpleFields } = updateProductDTO; + const { attributes, categoryId, categoryName, sku, components, siteSkus, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); // 解析属性输入(按 id 或 dictName 创建/关联字典项) let categoryItem: Category | null = null; @@ -994,6 +1006,22 @@ export class ProductService { product.type = updateProductDTO.type as any; } + // 处理站点 SKU 更新 + if (siteSkus !== undefined) { + if (Array.isArray(siteSkus)) { + // 转换为 SiteSku 实体数组 + product.siteSkus = siteSkus.map(sku => { + const siteSku = new SiteSku(); + siteSku.sku = sku; + siteSku.isOld = false; + return siteSku; + }); + } else { + // 如果不是数组,清空siteSkus + product.siteSkus = []; + } + } + // 保存更新后的产品 const saved = await this.productModel.save(product); @@ -1001,9 +1029,12 @@ export class ProductService { if (updateProductDTO.components !== undefined) { // 如果 components 为空数组,则删除所有组件? setProductComponents 会处理 await this.setProductComponents(saved.id, updateProductDTO.components); + // 重新加载带组件的产品 + return await this.productModel.findOne({ where: { id: saved.id }, relations: ['attributes', 'attributes.dict', 'category', 'components'] }); } - return saved; + // 重新加载产品以获取完整的关联关系 + return await this.productModel.findOne({ where: { id: saved.id }, relations: ['attributes', 'attributes.dict', 'category'] }); } async batchUpdateProduct( @@ -1014,13 +1045,14 @@ export class ProductService { throw new Error('未选择任何产品'); } - // 检查 updateData 中是否有复杂字段 (attributes, categoryId, type, sku) + // 检查 updateData 中是否有复杂字段 (attributes, categoryId, type, sku, siteSkus) // 如果包含复杂字段,需要复用 updateProduct 的逻辑 const hasComplexFields = updateData.attributes !== undefined || updateData.categoryId !== undefined || updateData.type !== undefined || - updateData.sku !== undefined; + updateData.sku !== undefined || + updateData.siteSkus !== undefined; if (hasComplexFields) { // 循环调用 updateProduct @@ -1032,7 +1064,7 @@ export class ProductService { } } else { // 简单字段,直接批量更新以提高性能 - // UpdateProductDTO 里的简单字段: name, nameCn, description, shortDescription, price, promotionPrice, image, siteSkus + // UpdateProductDTO 里的简单字段: name, nameCn, description, shortDescription, price, promotionPrice, image const simpleUpdate: any = {}; if (updateData.name !== undefined) simpleUpdate.name = updateData.name; @@ -1042,7 +1074,6 @@ export class ProductService { if (updateData.price !== undefined) simpleUpdate.price = updateData.price; if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice; if (updateData.image !== undefined) simpleUpdate.image = updateData.image; - if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus; if (Object.keys(simpleUpdate).length > 0) { await this.productModel.update({ id: In(ids) }, simpleUpdate); @@ -1923,9 +1954,10 @@ export class ProductService { } // 导出所有产品为 CSV 文本 - async exportProductsCSV(): Promise { + async exportProductsCSV(params?: UnifiedSearchParamsDTO): Promise { // 查询所有产品及其属性(包含字典关系)、组成和分类 const products = await this.productModel.find({ + where: params.where, relations: ['attributes', 'attributes.dict', 'components', 'category'], order: { id: 'ASC' }, }); @@ -2150,7 +2182,7 @@ export class ProductService { if (!product) { throw new Error(`产品 ID ${productId} 不存在`); } - return product.siteSkus || []; + return product.siteSkus.map(({sku})=>sku) || []; } // 绑定产品的站点SKU列表 @@ -2162,9 +2194,17 @@ export class ProductService { const normalizedSiteSkus = (siteSkus || []) .map(c => String(c).trim()) .filter(c => c.length > 0); - product.siteSkus = normalizedSiteSkus; + + // 更新产品的站点SKU列表 + product.siteSkus = normalizedSiteSkus.map(sku => { + const siteSku = new SiteSku(); + siteSku.sku = sku; + siteSku.isOld = false; + return siteSku; + }); + await this.productModel.save(product); - return product.siteSkus || []; + return normalizedSiteSkus; } /** diff --git a/src/service/stock.service.ts b/src/service/stock.service.ts index bc7d4e1..05bed63 100644 --- a/src/service/stock.service.ts +++ b/src/service/stock.service.ts @@ -547,7 +547,7 @@ export class StockService { qb .select([ 'ti.transferId AS transferId', - "JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.productName,'sku', ti.sku, 'quantity', ti.quantity)) AS items", + "JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.name,'sku', ti.sku, 'quantity', ti.quantity)) AS items", ]) .from(TransferItem, 'ti') .groupBy('ti.transferId'),