refactor(实体和服务): 统一将productSku字段重命名为sku

重构实体类和服务层代码,将productSku字段统一更名为sku以保持命名一致性
修改涉及库存、订单、采购等多个模块的查询和更新逻辑
同时将产品类型默认值从simple改为single,并优化相关条件判断
This commit is contained in:
tikkhun 2025-12-01 10:59:49 +08:00
parent 8b31da07a0
commit e4fc195b8d
13 changed files with 142 additions and 114 deletions

24
output.log Normal file
View File

@ -0,0 +1,24 @@
> my-midway-project@1.0.0 dev
> cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js

[10:37:17 AM] Starting compilation in watch mode...
[10:37:19 AM] Found 0 errors. Watching for file changes.
2025-12-01 10:37:20.106 INFO 58678 [SyncProductJob] start job SyncProductJob
2025-12-01 10:37:20.106 INFO 58678 [SyncShipmentJob] start job SyncShipmentJob
2025-12-01 10:37:20.109 INFO 58678 [SyncProductJob] complete job SyncProductJob
Node.js server started in 732 ms
➜ Local: http://127.0.0.1:7001/
➜ Network: http://192.168.5.100:7001/ 
2025-12-01 10:37:20.110 INFO 58678 [SyncShipmentJob] complete job SyncShipmentJob

View File

@ -24,13 +24,13 @@ export class QueryStockDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productSku: string; sku: string;
@ApiProperty({ description: '按库存点ID排序', required: false }) @ApiProperty({ description: '按库存点ID排序', required: false })
@Rule(RuleType.number().allow(null)) @Rule(RuleType.number().allow(null))
sortPointId?: number; sortPointId?: number;
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", productSku: "desc" }', required: false }) @ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }', required: false })
@Rule(RuleType.object().allow(null)) @Rule(RuleType.object().allow(null))
order?: Record<string, 'asc' | 'desc'>; order?: Record<string, 'asc' | 'desc'>;
} }
@ -58,7 +58,7 @@ export class QueryStockRecordDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productSku: string; sku: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
@ -132,7 +132,7 @@ export class UpdateStockDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productSku: string; sku: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.number()) @Rule(RuleType.number())

View File

@ -23,6 +23,11 @@ export class Product {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
// 类型 主要用来区分混装和单品 单品死
@ApiProperty({ description: '类型' })
@Column({ length: 16, default: 'single' })
type: string;
@ApiProperty({ @ApiProperty({
example: 'ZYN 6MG WINTERGREEN', example: 'ZYN 6MG WINTERGREEN',
description: '产品名称', description: '产品名称',
@ -49,10 +54,7 @@ export class Product {
@ApiProperty({ description: '价格', example: 99.99 }) @ApiProperty({ description: '价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
price: number; price: number;
// 类型 主要用来区分混装和单品 单品死
@ApiProperty({ description: '类型' })
@Column({ length: 16, default: 'simple' })
type: string;
// 促销价格 // 促销价格
@ApiProperty({ description: '促销价格', example: 99.99 }) @ApiProperty({ description: '促销价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })

View File

@ -14,7 +14,7 @@ export class ProductStockComponent {
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' }) @ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
@Column({ type: 'varchar', length: 64 }) @Column({ type: 'varchar', length: 64 })
productSku: string; sku: string;
@ApiProperty({ type: Number, description: '组成数量' }) @ApiProperty({ type: Number, description: '组成数量' })
@Column({ type: 'int', default: 1 }) @Column({ type: 'int', default: 1 })

View File

@ -10,7 +10,7 @@ export class PurchaseOrderItem {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()

View File

@ -20,7 +20,7 @@ export class Stock {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: Number }) @ApiProperty({ type: Number })
@Column() @Column()

View File

@ -20,7 +20,7 @@ export class StockRecord {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: StockRecordOperationType }) @ApiProperty({ type: StockRecordOperationType })
@Column({ type: 'enum', enum: StockRecordOperationType }) @Column({ type: 'enum', enum: StockRecordOperationType })

View File

@ -9,7 +9,7 @@ export class TransferItem {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()

View File

@ -527,14 +527,14 @@ export class LogisticsService {
const stock = await stockRepo.findOne({ const stock = await stockRepo.findOne({
where: { where: {
stockPointId: orderShipments[0].stockPointId, stockPointId: orderShipments[0].stockPointId,
productSku: item.sku, sku: item.sku,
}, },
}); });
stock.quantity += item.quantity; stock.quantity += item.quantity;
await stockRepo.save(stock); await stockRepo.save(stock);
await stockRecordRepo.save({ await stockRecordRepo.save({
stockPointId: orderShipments[0].stockPointId, stockPointId: orderShipments[0].stockPointId,
productSku: item.sku, sku: item.sku,
operationType: StockRecordOperationType.IN, operationType: StockRecordOperationType.IN,
quantityChange: item.quantity, quantityChange: item.quantity,
operatorId: userId, operatorId: userId,

View File

@ -217,7 +217,7 @@ export class OrderService {
for (const item of items) { for (const item of items) {
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = stockPointId; updateStock.stockPointId = stockPointId;
updateStock.productSku = item.sku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.OUT; updateStock.operationType = StockRecordOperationType.OUT;
updateStock.operatorId = 1; updateStock.operatorId = 1;

View File

@ -177,17 +177,19 @@ export class ProductService {
const [items, total] = await qb.getManyAndCount(); const [items, total] = await qb.getManyAndCount();
// 中文注释:根据类型填充组成信息 // 中文注释:根据类型填充组成信息
for (const p of items) { for (const product of items) {
if (p.type === 'simple') { if (product.type === 'single') {
// 中文注释:单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成 // 中文注释:单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
const comp = new ProductStockComponent(); const component = new ProductStockComponent();
comp.productId = p.id; component.productId = product.id;
comp.productSku = p.sku; component.sku = product.sku;
comp.quantity = 1; component.quantity = 1;
p.components = [comp]; product.components = [component];
} else { } else {
// 中文注释:混装商品返回持久化的 SKU 组成 // 中文注释:混装商品返回持久化的 SKU 组成
p.components = await this.productStockComponentModel.find({ where: { productId: p.id } }); product.components = await this.productStockComponentModel.find({
where: { productId: product.id },
});
} }
} }
@ -273,7 +275,7 @@ export class ProductService {
product.description = description; product.description = description;
product.attributes = resolvedAttributes; product.attributes = resolvedAttributes;
// 条件判断(中文注释:设置商品类型,默认 simple // 条件判断(中文注释:设置商品类型,默认 simple
product.type = (createProductDTO.type as any) || 'simple'; product.type = (createProductDTO.type as any) || 'single';
// 生成或设置 SKU中文注释基于属性字典项的 name 生成) // 生成或设置 SKU中文注释基于属性字典项的 name 生成)
if (sku) { if (sku) {
@ -387,10 +389,10 @@ export class ProductService {
let components: ProductStockComponent[] = []; let components: ProductStockComponent[] = [];
// 条件判断(中文注释:单品 simple 不持久化组成,按 SKU 动态返回单条组成) // 条件判断(中文注释:单品 simple 不持久化组成,按 SKU 动态返回单条组成)
if (product.type === 'simple') { if (product.type === 'single') {
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
comp.productId = productId; comp.productId = productId;
comp.productSku = product.sku; comp.sku = product.sku;
comp.quantity = 1; comp.quantity = 1;
components = [comp]; components = [comp];
} else { } else {
@ -399,14 +401,14 @@ export class ProductService {
} }
// 中文注释:获取所有组件的 SKU 列表 // 中文注释:获取所有组件的 SKU 列表
const skus = components.map(c => c.productSku); const skus = components.map(c => c.sku);
if (skus.length === 0) { if (skus.length === 0) {
return components; return components;
} }
// 中文注释:查询这些 SKU 的库存信息 // 中文注释:查询这些 SKU 的库存信息
const stocks = await this.stockModel.find({ const stocks = await this.stockModel.find({
where: { productSku: In(skus) }, where: { sku: In(skus) },
}); });
// 中文注释:获取所有相关的库存点 ID // 中文注释:获取所有相关的库存点 ID
@ -419,12 +421,12 @@ export class ProductService {
// 中文注释:将库存信息按 SKU 分组 // 中文注释:将库存信息按 SKU 分组
const stockMap = stocks.reduce((map, stock) => { const stockMap = stocks.reduce((map, stock) => {
if (!map[stock.productSku]) { if (!map[stock.sku]) {
map[stock.productSku] = []; map[stock.sku] = [];
} }
const stockPoint = stockPointMap[stock.stockPointId]; const stockPoint = stockPointMap[stock.stockPointId];
if (stockPoint) { if (stockPoint) {
map[stock.productSku].push({ map[stock.sku].push({
name: stockPoint.name, name: stockPoint.name,
quantity: stock.quantity, quantity: stock.quantity,
}); });
@ -436,7 +438,7 @@ export class ProductService {
const componentsWithStock = components.map(comp => { const componentsWithStock = components.map(comp => {
return { return {
...comp, ...comp,
stock: stockMap[comp.productSku] || [], stock: stockMap[comp.sku] || [],
}; };
}); });
@ -452,7 +454,7 @@ export class ProductService {
const product = await this.productModel.findOne({ where: { id: productId } }); const product = await this.productModel.findOne({ where: { id: productId } });
if (!product) throw new Error(`产品 ID ${productId} 不存在`); if (!product) throw new Error(`产品 ID ${productId} 不存在`);
// 条件判断(中文注释:单品 simple 不允许手动设置组成) // 条件判断(中文注释:单品 simple 不允许手动设置组成)
if (product.type === 'simple') { if (product.type === 'single') {
throw new Error('单品无需设置组成'); throw new Error('单品无需设置组成');
} }
@ -472,7 +474,7 @@ export class ProductService {
} }
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
comp.productId = productId; comp.productId = productId;
comp.productSku = i.sku; comp.sku = i.sku;
comp.quantity = i.quantity; comp.quantity = i.quantity;
created.push(await this.productStockComponentModel.save(comp)); created.push(await this.productStockComponentModel.save(comp));
} }
@ -486,19 +488,19 @@ export class ProductService {
if (!product) throw new Error(`产品 ID ${productId} 不存在`); if (!product) throw new Error(`产品 ID ${productId} 不存在`);
// 中文注释:按 SKU 自动绑定 // 中文注释:按 SKU 自动绑定
// 条件判断simple 类型不持久化组成,直接返回单条基于 SKU 的组成 // 条件判断simple 类型不持久化组成,直接返回单条基于 SKU 的组成
if (product.type === 'simple') { if (product.type === 'single') {
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
comp.productId = productId; comp.productId = productId;
comp.productSku = product.sku; comp.sku = product.sku;
comp.quantity = 1; // 默认数量 1 comp.quantity = 1; // 默认数量 1
return [comp]; return [comp];
} }
// bundle 类型:若不存在则持久化一条基于 SKU 的组成 // bundle 类型:若不存在则持久化一条基于 SKU 的组成
const exist = await this.productStockComponentModel.findOne({ where: { productId, productSku: product.sku } }); const exist = await this.productStockComponentModel.findOne({ where: { productId, sku: product.sku } });
if (!exist) { if (!exist) {
const comp = new ProductStockComponent(); const comp = new ProductStockComponent();
comp.productId = productId; comp.productId = productId;
comp.productSku = product.sku; comp.sku = product.sku;
comp.quantity = 1; comp.quantity = 1;
await this.productStockComponentModel.save(comp); await this.productStockComponentModel.save(comp);
} }
@ -525,13 +527,13 @@ export class ProductService {
if (!product) { if (!product) {
throw new Error(`产品 ID ${id} 不存在`); throw new Error(`产品 ID ${id} 不存在`);
} }
const productSku = product.sku; const sku = product.sku;
// 查询 wp_product 表中是否存在与该 SKU 关联的产品 // 查询 wp_product 表中是否存在与该 SKU 关联的产品
const wpProduct = await this.wpProductModel const wpProduct = await this.wpProductModel
.createQueryBuilder('wp_product') .createQueryBuilder('wp_product')
.where('JSON_CONTAINS(wp_product.constitution, :sku)', { .where('JSON_CONTAINS(wp_product.constitution, :sku)', {
sku: JSON.stringify({ sku: productSku }), sku: JSON.stringify({ sku: sku }),
}) })
.getOne(); .getOne();
if (wpProduct) { if (wpProduct) {
@ -541,7 +543,7 @@ export class ProductService {
const variation = await this.variationModel const variation = await this.variationModel
.createQueryBuilder('variation') .createQueryBuilder('variation')
.where('JSON_CONTAINS(variation.constitution, :sku)', { .where('JSON_CONTAINS(variation.constitution, :sku)', {
sku: JSON.stringify({ sku: productSku }), sku: JSON.stringify({ sku: sku }),
}) })
.getOne(); .getOne();

View File

@ -656,10 +656,10 @@ export class StatisticsService {
const offset = (current - 1) * pageSize; const offset = (current - 1) * pageSize;
const countSql = ` const countSql = `
WITH product_list AS ( WITH product_list AS (
SELECT DISTINCT s.productSku SELECT DISTINCT s.sku
FROM stock s FROM stock s
LEFT JOIN stock_point sp ON s.stockPointId = sp.id LEFT JOIN stock_point sp ON s.stockPointId = sp.id
LEFT JOIN product p ON s.productSku = p.sku LEFT JOIN product p ON s.sku = p.sku
WHERE sp.ignore = FALSE WHERE sp.ignore = FALSE
${countnameFilter} ${countnameFilter}
) )
@ -674,27 +674,27 @@ export class StatisticsService {
const sql = ` const sql = `
WITH stock_summary AS ( WITH stock_summary AS (
SELECT SELECT
s.productSku, s.sku,
JSON_ARRAYAGG(JSON_OBJECT('id', sp.id, 'quantity', s.quantity)) AS stockDetails, JSON_ARRAYAGG(JSON_OBJECT('id', sp.id, 'quantity', s.quantity)) AS stockDetails,
SUM(s.quantity) AS totalStock, SUM(s.quantity) AS totalStock,
SUM(CASE WHEN sp.inCanada THEN s.quantity ELSE 0 END) AS caTotalStock SUM(CASE WHEN sp.inCanada THEN s.quantity ELSE 0 END) AS caTotalStock
FROM stock s FROM stock s
JOIN stock_point sp ON s.stockPointId = sp.id JOIN stock_point sp ON s.stockPointId = sp.id
WHERE sp.ignore = FALSE WHERE sp.ignore = FALSE
GROUP BY s.productSku GROUP BY s.sku
), ),
transfer_stock AS ( transfer_stock AS (
SELECT SELECT
ti.productSku, ti.sku,
SUM(ti.quantity) AS transitStock SUM(ti.quantity) AS transitStock
FROM transfer_item ti FROM transfer_item ti
JOIN transfer t ON ti.transferId = t.id JOIN transfer t ON ti.transferId = t.id
WHERE t.isCancel = FALSE AND t.isArrived = FALSE WHERE t.isCancel = FALSE AND t.isArrived = FALSE
GROUP BY ti.productSku GROUP BY ti.sku
), ),
30_sales_summary AS ( 30_sales_summary AS (
SELECT SELECT
os.sku AS productSku, os.sku AS sku,
SUM(os.quantity) AS totalSales SUM(os.quantity) AS totalSales
FROM order_sale os FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id JOIN \`order\` o ON os.orderId = o.id
@ -704,7 +704,7 @@ export class StatisticsService {
), ),
15_sales_summary AS ( 15_sales_summary AS (
SELECT SELECT
os.sku AS productSku, os.sku AS sku,
2 * SUM(os.quantity) AS totalSales 2 * SUM(os.quantity) AS totalSales
FROM order_sale os FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id JOIN \`order\` o ON os.orderId = o.id
@ -714,36 +714,36 @@ export class StatisticsService {
), ),
sales_max_summary AS ( sales_max_summary AS (
SELECT SELECT
s30.productSku AS productSku, s30.sku AS sku,
COALESCE(s30.totalSales, 0) AS totalSales_30, COALESCE(s30.totalSales, 0) AS totalSales_30,
COALESCE(s15.totalSales, 0) AS totalSales_15, COALESCE(s15.totalSales, 0) AS totalSales_15,
GREATEST(COALESCE(s30.totalSales, 0), COALESCE(s15.totalSales, 0)) AS maxSales GREATEST(COALESCE(s30.totalSales, 0), COALESCE(s15.totalSales, 0)) AS maxSales
FROM 30_sales_summary s30 FROM 30_sales_summary s30
LEFT JOIN 15_sales_summary s15 LEFT JOIN 15_sales_summary s15
ON s30.productSku = s15.productSku ON s30.sku = s15.sku
UNION ALL UNION ALL
SELECT SELECT
s15.productSku AS productSku, s15.sku AS sku,
0 AS totalSales_30, 0 AS totalSales_30,
COALESCE(s15.totalSales, 0) AS totalSales_15, COALESCE(s15.totalSales, 0) AS totalSales_15,
COALESCE(s15.totalSales, 0) AS maxSales COALESCE(s15.totalSales, 0) AS maxSales
FROM 15_sales_summary s15 FROM 15_sales_summary s15
LEFT JOIN 30_sales_summary s30 LEFT JOIN 30_sales_summary s30
ON s30.productSku = s15.productSku ON s30.sku = s15.sku
WHERE s30.productSku IS NULL WHERE s30.sku IS NULL
), ),
product_name_summary AS ( product_name_summary AS (
SELECT SELECT
p.sku AS productSku, p.sku AS sku,
COALESCE(MAX(os.name), MAX(p.name)) AS productName COALESCE(MAX(os.name), MAX(p.name)) AS productName
FROM product p FROM product p
LEFT JOIN order_sale os ON p.sku = os.sku LEFT JOIN order_sale os ON p.sku = os.sku
GROUP BY p.sku GROUP BY p.sku
) )
SELECT SELECT
ss.productSku, ss.sku,
ss.stockDetails, ss.stockDetails,
COALESCE(ts.transitStock, 0) AS transitStock, COALESCE(ts.transitStock, 0) AS transitStock,
(COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock,
@ -761,9 +761,9 @@ export class StatisticsService {
sales.maxSales * 4 AS restockQuantity, sales.maxSales * 4 AS restockQuantity,
pns.productName pns.productName
FROM stock_summary ss FROM stock_summary ss
LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku LEFT JOIN transfer_stock ts ON ss.sku = ts.sku
LEFT JOIN sales_max_summary sales ON ss.productSku = sales.productSku LEFT JOIN sales_max_summary sales ON ss.sku = sales.sku
LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku LEFT JOIN product_name_summary pns ON ss.sku = pns.sku
WHERE 1 = 1 WHERE 1 = 1
${nameFilter} ${nameFilter}
ORDER BY caAvailableDays ORDER BY caAvailableDays
@ -791,10 +791,10 @@ export class StatisticsService {
const offset = (current - 1) * pageSize; const offset = (current - 1) * pageSize;
const countSql = ` const countSql = `
WITH product_list AS ( WITH product_list AS (
SELECT DISTINCT s.productSku SELECT DISTINCT s.sku
FROM stock s FROM stock s
LEFT JOIN stock_point sp ON s.stockPointId = sp.id LEFT JOIN stock_point sp ON s.stockPointId = sp.id
LEFT JOIN product p ON s.productSku = p.sku LEFT JOIN product p ON s.sku = p.sku
WHERE sp.ignore = FALSE WHERE sp.ignore = FALSE
${countnameFilter} ${countnameFilter}
) )
@ -810,36 +810,36 @@ export class StatisticsService {
const sql = ` const sql = `
WITH stock_summary AS ( WITH stock_summary AS (
SELECT SELECT
s.productSku, s.sku,
SUM(s.quantity) AS totalStock SUM(s.quantity) AS totalStock
FROM stock s FROM stock s
JOIN stock_point sp ON s.stockPointId = sp.id JOIN stock_point sp ON s.stockPointId = sp.id
WHERE sp.ignore = FALSE WHERE sp.ignore = FALSE
GROUP BY s.productSku GROUP BY s.sku
), ),
transfer_stock AS ( transfer_stock AS (
SELECT SELECT
ti.productSku, ti.sku,
SUM(ti.quantity) AS transitStock SUM(ti.quantity) AS transitStock
FROM transfer_item ti FROM transfer_item ti
JOIN transfer t ON ti.transferId = t.id JOIN transfer t ON ti.transferId = t.id
WHERE t.isCancel = FALSE AND t.isArrived = FALSE WHERE t.isCancel = FALSE AND t.isArrived = FALSE
GROUP BY ti.productSku GROUP BY ti.sku
), ),
b_sales_data_raw As ( b_sales_data_raw As (
SELECT SELECT
sr.productSku, sr.sku,
DATE_FORMAT(sr.createdAt, '%Y-%m') AS month, DATE_FORMAT(sr.createdAt, '%Y-%m') AS month,
SUM(sr.quantityChange) AS sales SUM(sr.quantityChange) AS sales
FROM stock_record sr FROM stock_record sr
JOIN stock_point sp ON sr.stockPointId = sp.id JOIN stock_point sp ON sr.stockPointId = sp.id
WHERE sp.isB WHERE sp.isB
AND sr.createdAt >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01') AND sr.createdAt >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01')
GROUP BY sr.productSku, month GROUP BY sr.sku, month
), ),
sales_data_raw AS ( sales_data_raw AS (
SELECT SELECT
os.sku AS productSku, os.sku AS sku,
DATE_FORMAT(o.date_paid, '%Y-%m') AS month, DATE_FORMAT(o.date_paid, '%Y-%m') AS month,
SUM(CASE WHEN DAY(o.date_paid) <= 10 THEN os.quantity ELSE 0 END) AS early_sales, SUM(CASE WHEN DAY(o.date_paid) <= 10 THEN os.quantity ELSE 0 END) AS early_sales,
SUM(CASE WHEN DAY(o.date_paid) > 10 AND DAY(o.date_paid) <= 20 THEN os.quantity ELSE 0 END) AS mid_sales, SUM(CASE WHEN DAY(o.date_paid) > 10 AND DAY(o.date_paid) <= 20 THEN os.quantity ELSE 0 END) AS mid_sales,
@ -852,7 +852,7 @@ export class StatisticsService {
), ),
monthly_sales_summary AS ( monthly_sales_summary AS (
SELECT SELECT
sdr.productSku, sdr.sku,
JSON_ARRAYAGG( JSON_ARRAYAGG(
JSON_OBJECT( JSON_OBJECT(
'month', sdr.month, 'month', sdr.month,
@ -863,12 +863,12 @@ export class StatisticsService {
) )
) AS sales_data ) AS sales_data
FROM sales_data_raw sdr FROM sales_data_raw sdr
LEFT JOIN b_sales_data_raw b ON sdr.productSku = b.productSku AND sdr.month = b.month LEFT JOIN b_sales_data_raw b ON sdr.sku = b.sku AND sdr.month = b.month
GROUP BY sdr.productSku GROUP BY sdr.sku
), ),
sales_summary AS ( sales_summary AS (
SELECT SELECT
os.sku AS productSku, os.sku AS sku,
SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 30 DAY THEN os.quantity ELSE 0 END) AS last_30_days_sales, SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 30 DAY THEN os.quantity ELSE 0 END) AS last_30_days_sales,
SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 15 DAY THEN os.quantity ELSE 0 END) AS last_15_days_sales, SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 15 DAY THEN os.quantity ELSE 0 END) AS last_15_days_sales,
SUM(CASE WHEN DATE_FORMAT(o.date_paid, '%Y-%m') = DATE_FORMAT(CURDATE() - INTERVAL 1 MONTH, '%Y-%m') THEN os.quantity ELSE 0 END) AS last_month_sales SUM(CASE WHEN DATE_FORMAT(o.date_paid, '%Y-%m') = DATE_FORMAT(CURDATE() - INTERVAL 1 MONTH, '%Y-%m') THEN os.quantity ELSE 0 END) AS last_month_sales
@ -880,14 +880,14 @@ export class StatisticsService {
), ),
product_name_summary AS ( product_name_summary AS (
SELECT SELECT
p.sku AS productSku, p.sku AS sku,
COALESCE(MAX(os.name), MAX(p.name)) AS productName COALESCE(MAX(os.name), MAX(p.name)) AS productName
FROM product p FROM product p
LEFT JOIN order_sale os ON p.sku = os.sku LEFT JOIN order_sale os ON p.sku = os.sku
GROUP BY p.sku GROUP BY p.sku
) )
SELECT SELECT
ss.productSku, ss.sku,
(COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock,
ms.sales_data AS monthlySalesData, ms.sales_data AS monthlySalesData,
pns.productName, pns.productName,
@ -900,10 +900,10 @@ export class StatisticsService {
ELSE NULL ELSE NULL
END AS stock_ratio END AS stock_ratio
FROM stock_summary ss FROM stock_summary ss
LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku LEFT JOIN transfer_stock ts ON ss.sku = ts.sku
LEFT JOIN monthly_sales_summary ms ON ss.productSku = ms.productSku LEFT JOIN monthly_sales_summary ms ON ss.sku = ms.sku
LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku LEFT JOIN product_name_summary pns ON ss.sku = pns.sku
LEFT JOIN sales_summary ssum ON ss.productSku = ssum.productSku LEFT JOIN sales_summary ssum ON ss.sku = ssum.sku
WHERE 1 = 1 WHERE 1 = 1
${nameFilter} ${nameFilter}
ORDER BY ORDER BY

View File

@ -167,7 +167,7 @@ export class StockService {
qb qb
.select([ .select([
'poi.purchaseOrderId AS purchaseOrderId', 'poi.purchaseOrderId AS purchaseOrderId',
"JSON_ARRAYAGG(JSON_OBJECT('id', poi.id, 'productName', poi.productName,'productSku', poi.productSku, 'quantity', poi.quantity, 'price', poi.price)) AS items", "JSON_ARRAYAGG(JSON_OBJECT('id', poi.id, 'productName', poi.productName,'sku', poi.sku, 'quantity', poi.quantity, 'price', poi.price)) AS items",
]) ])
.from(PurchaseOrderItem, 'poi') .from(PurchaseOrderItem, 'poi')
.groupBy('poi.purchaseOrderId'), .groupBy('poi.purchaseOrderId'),
@ -191,7 +191,7 @@ export class StockService {
async hasStockBySku(sku: string): Promise<boolean> { async hasStockBySku(sku: string): Promise<boolean> {
const count = await this.stockModel const count = await this.stockModel
.createQueryBuilder('stock') .createQueryBuilder('stock')
.where('stock.productSku = :sku', { sku }) .where('stock.sku = :sku', { sku })
.andWhere('stock.quantity > 0') .andWhere('stock.quantity > 0')
.getCount(); .getCount();
return count > 0; return count > 0;
@ -217,7 +217,7 @@ export class StockService {
for (const item of items) { for (const item of items) {
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = purchaseOrder.stockPointId; updateStock.stockPointId = purchaseOrder.stockPointId;
updateStock.productSku = item.productSku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN; updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId; updateStock.operatorId = userId;
@ -240,7 +240,7 @@ export class StockService {
// 获取库存列表 // 获取库存列表
async getStocks(query: QueryStockDTO) { async getStocks(query: QueryStockDTO) {
const { current = 1, pageSize = 10, productName, productSku } = query; const { current = 1, pageSize = 10, productName, sku } = query;
const nameKeywords = productName const nameKeywords = productName
? productName.split(' ').filter(Boolean) ? productName.split(' ').filter(Boolean)
: []; : [];
@ -249,31 +249,31 @@ export class StockService {
.createQueryBuilder('stock') .createQueryBuilder('stock')
.select([ .select([
// 'stock.id as id', // 'stock.id as id',
'stock.productSku as productSku', 'stock.sku as sku',
'product.name as productName', 'product.name as productName',
'product.nameCn as productNameCn', 'product.nameCn as productNameCn',
'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint', 'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint',
'MIN(stock.updatedAt) as updatedAt', 'MIN(stock.updatedAt) as updatedAt',
'MAX(stock.createdAt) as createdAt', 'MAX(stock.createdAt) as createdAt',
]) ])
.leftJoin(Product, 'product', 'product.sku = stock.productSku') .leftJoin(Product, 'product', 'product.sku = stock.sku')
.groupBy('stock.productSku') .groupBy('stock.sku')
.addGroupBy('product.name') .addGroupBy('product.name')
.addGroupBy('product.nameCn'); .addGroupBy('product.nameCn');
let totalQueryBuilder = this.stockModel let totalQueryBuilder = this.stockModel
.createQueryBuilder('stock') .createQueryBuilder('stock')
.select('COUNT(DISTINCT stock.productSku)', 'count') .select('COUNT(DISTINCT stock.sku)', 'count')
.leftJoin(Product, 'product', 'product.sku = stock.productSku'); .leftJoin(Product, 'product', 'product.sku = stock.sku');
if (productSku) { if (sku) {
queryBuilder.andWhere('stock.productSku = :productSku', { productSku }); queryBuilder.andWhere('stock.sku = :sku', { sku });
totalQueryBuilder.andWhere('stock.productSku = :productSku', { productSku }); totalQueryBuilder.andWhere('stock.sku = :sku', { sku });
} }
if (nameKeywords.length) { if (nameKeywords.length) {
nameKeywords.forEach((name, index) => { nameKeywords.forEach((name, index) => {
queryBuilder.andWhere( queryBuilder.andWhere(
`EXISTS ( `EXISTS (
SELECT 1 FROM product p SELECT 1 FROM product p
WHERE p.sku = stock.productSku WHERE p.sku = stock.sku
AND p.name LIKE :name${index} AND p.name LIKE :name${index}
)`, )`,
{ [`name${index}`]: `%${name}%` } { [`name${index}`]: `%${name}%` }
@ -281,7 +281,7 @@ export class StockService {
totalQueryBuilder.andWhere( totalQueryBuilder.andWhere(
`EXISTS ( `EXISTS (
SELECT 1 FROM product p SELECT 1 FROM product p
WHERE p.sku = stock.productSku WHERE p.sku = stock.sku
AND p.name LIKE :name${index} AND p.name LIKE :name${index}
)`, )`,
{ [`name${index}`]: `%${name}%` } { [`name${index}`]: `%${name}%` }
@ -291,7 +291,7 @@ export class StockService {
if (query.order) { if (query.order) {
const sortFieldMap: Record<string, string> = { const sortFieldMap: Record<string, string> = {
productName: 'product.name', productName: 'product.name',
productSku: 'stock.productSku', sku: 'stock.sku',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdAt: 'createdAt', createdAt: 'createdAt',
}; };
@ -335,15 +335,15 @@ export class StockService {
const transfer = await this.transferModel const transfer = await this.transferModel
.createQueryBuilder('t') .createQueryBuilder('t')
.select(['ti.productSku as productSku', 'SUM(ti.quantity) as quantity']) .select(['ti.sku as sku', 'SUM(ti.quantity) as quantity'])
.leftJoin(TransferItem, 'ti', 'ti.transferId = t.id') .leftJoin(TransferItem, 'ti', 'ti.transferId = t.id')
.where('!t.isArrived and !t.isCancel and !t.isLost') .where('!t.isArrived and !t.isCancel and !t.isLost')
.groupBy('ti.productSku') .groupBy('ti.sku')
.getRawMany(); .getRawMany();
for (const item of items) { for (const item of items) {
item.inTransitQuantity = item.inTransitQuantity =
transfer.find(t => t.productSku === item.productSku)?.quantity || 0; transfer.find(t => t.sku === item.sku)?.quantity || 0;
} }
return { return {
@ -361,10 +361,10 @@ export class StockService {
const stocks = await this.stockModel const stocks = await this.stockModel
.createQueryBuilder('stock') .createQueryBuilder('stock')
.select('stock.productSku', 'productSku') .select('stock.sku', 'sku')
.addSelect('SUM(stock.quantity)', 'totalQuantity') .addSelect('SUM(stock.quantity)', 'totalQuantity')
.where('stock.productSku IN (:...skus)', { skus }) .where('stock.sku IN (:...skus)', { skus })
.groupBy('stock.productSku') .groupBy('stock.sku')
.getRawMany(); .getRawMany();
return stocks; return stocks;
@ -374,7 +374,7 @@ export class StockService {
async updateStock(data: UpdateStockDTO) { async updateStock(data: UpdateStockDTO) {
const { const {
stockPointId, stockPointId,
productSku, sku,
quantityChange, quantityChange,
operationType, operationType,
operatorId, operatorId,
@ -383,13 +383,13 @@ export class StockService {
const stock = await this.stockModel.findOneBy({ const stock = await this.stockModel.findOneBy({
stockPointId, stockPointId,
productSku, sku,
}); });
if (!stock) { if (!stock) {
// 如果库存不存在,则直接新增 // 如果库存不存在,则直接新增
const newStock = this.stockModel.create({ const newStock = this.stockModel.create({
stockPointId, stockPointId,
productSku, sku,
quantity: operationType === 'in' ? quantityChange : -quantityChange, quantity: operationType === 'in' ? quantityChange : -quantityChange,
}); });
await this.stockModel.save(newStock); await this.stockModel.save(newStock);
@ -406,7 +406,7 @@ export class StockService {
// 记录库存变更日志 // 记录库存变更日志
const stockRecord = this.stockRecordModel.create({ const stockRecord = this.stockRecordModel.create({
stockPointId, stockPointId,
productSku, sku,
operationType, operationType,
quantityChange, quantityChange,
operatorId, operatorId,
@ -421,7 +421,7 @@ export class StockService {
current = 1, current = 1,
pageSize = 10, pageSize = 10,
stockPointId, stockPointId,
productSku, sku,
productName, productName,
operationType, operationType,
startDate, startDate,
@ -430,14 +430,14 @@ export class StockService {
const where: any = {}; const where: any = {};
if (stockPointId) where.stockPointId = stockPointId; if (stockPointId) where.stockPointId = stockPointId;
if (productSku) where.productSku = productSku; if (sku) where.sku = sku;
if (operationType) where.operationType = operationType; if (operationType) where.operationType = operationType;
if (startDate) where.createdAt = MoreThan(startDate); if (startDate) where.createdAt = MoreThan(startDate);
if (endDate) where.createdAt = LessThan(endDate); if (endDate) where.createdAt = LessThan(endDate);
if (startDate && endDate) where.createdAt = Between(startDate, endDate); if (startDate && endDate) where.createdAt = Between(startDate, endDate);
const queryBuilder = this.stockRecordModel const queryBuilder = this.stockRecordModel
.createQueryBuilder('stock_record') .createQueryBuilder('stock_record')
.leftJoin(Product, 'product', 'product.sku = stock_record.productSku') .leftJoin(Product, 'product', 'product.sku = stock_record.sku')
.leftJoin(User, 'user', 'stock_record.operatorId = user.id') .leftJoin(User, 'user', 'stock_record.operatorId = user.id')
.leftJoin(StockPoint, 'sp', 'sp.id = stock_record.stockPointId') .leftJoin(StockPoint, 'sp', 'sp.id = stock_record.stockPointId')
.select([ .select([
@ -470,7 +470,7 @@ export class StockService {
// for (const item of items) { // for (const item of items) {
// const stock = await this.stockModel.findOneBy({ // const stock = await this.stockModel.findOneBy({
// stockPointId: sourceStockPointId, // stockPointId: sourceStockPointId,
// productSku: item.productSku, // sku: item.sku,
// }); // });
// if (!stock || stock.quantity < item.quantity) // if (!stock || stock.quantity < item.quantity)
// throw new Error(`${item.productName} 库存不足`); // throw new Error(`${item.productName} 库存不足`);
@ -496,7 +496,7 @@ export class StockService {
item.transferId = transfer.id; item.transferId = transfer.id;
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = sourceStockPointId; updateStock.stockPointId = sourceStockPointId;
updateStock.productSku = item.productSku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.OUT; updateStock.operationType = StockRecordOperationType.OUT;
updateStock.operatorId = userId; updateStock.operatorId = userId;
@ -530,7 +530,7 @@ export class StockService {
qb qb
.select([ .select([
'ti.transferId AS transferId', 'ti.transferId AS transferId',
"JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.productName,'productSku', ti.productSku, 'quantity', ti.quantity)) AS items", "JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.productName,'sku', ti.sku, 'quantity', ti.quantity)) AS items",
]) ])
.from(TransferItem, 'ti') .from(TransferItem, 'ti')
.groupBy('ti.transferId'), .groupBy('ti.transferId'),
@ -561,7 +561,7 @@ export class StockService {
for (const item of items) { for (const item of items) {
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = transfer.sourceStockPointId; updateStock.stockPointId = transfer.sourceStockPointId;
updateStock.productSku = item.productSku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN; updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId; updateStock.operatorId = userId;
@ -584,7 +584,7 @@ export class StockService {
for (const item of items) { for (const item of items) {
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = transfer.destStockPointId; updateStock.stockPointId = transfer.destStockPointId;
updateStock.productSku = item.productSku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN; updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId; updateStock.operatorId = userId;
@ -619,7 +619,7 @@ export class StockService {
item.transferId = transfer.id; item.transferId = transfer.id;
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = sourceStockPointId; updateStock.stockPointId = sourceStockPointId;
updateStock.productSku = item.productSku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN; updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId; updateStock.operatorId = userId;
@ -632,7 +632,7 @@ export class StockService {
item.transferId = transfer.id; item.transferId = transfer.id;
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = sourceStockPointId; updateStock.stockPointId = sourceStockPointId;
updateStock.productSku = item.productSku; updateStock.sku = item.sku;
updateStock.quantityChange = item.quantity; updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.OUT; updateStock.operationType = StockRecordOperationType.OUT;
updateStock.operatorId = userId; updateStock.operatorId = userId;