refactor(supply-chain): retire sku operator source
This commit is contained in:
parent
032b609f1b
commit
0d6d080687
@ -0,0 +1,52 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TEMP TABLE "_retired_js_sku_info_manage_archives" ON COMMIT DROP AS
|
||||
SELECT
|
||||
"id",
|
||||
"fieldSourceMap"->'opsHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE' AS "clearOpsHead",
|
||||
"fieldSourceMap"->'opsDeptHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE' AS "clearOpsDeptHead",
|
||||
"fieldSourceMap"->'designer'->>'sourceCode' = 'JST_SKU_INFO_MANAGE' AS "clearDesigner",
|
||||
(
|
||||
"fieldSourceMap"->'opsHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE'
|
||||
AND "fieldSourceMap"->'opsDirector'->>'sourceCode' = 'SYSTEM_USER_DEPARTMENT'
|
||||
) AS "clearOpsDirector",
|
||||
ARRAY_REMOVE(ARRAY[
|
||||
CASE WHEN "fieldSourceMap"->'opsHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE' THEN 'opsHead' END,
|
||||
CASE WHEN "fieldSourceMap"->'opsDeptHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE' THEN 'opsDeptHead' END,
|
||||
CASE WHEN "fieldSourceMap"->'designer'->>'sourceCode' = 'JST_SKU_INFO_MANAGE' THEN 'designer' END,
|
||||
CASE WHEN
|
||||
"fieldSourceMap"->'opsHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE'
|
||||
AND "fieldSourceMap"->'opsDirector'->>'sourceCode' = 'SYSTEM_USER_DEPARTMENT'
|
||||
THEN 'opsDirector'
|
||||
END
|
||||
], NULL)::text[] AS "sourceKeys"
|
||||
FROM "SupplyChainProductArchive"
|
||||
WHERE
|
||||
"fieldSourceMap"->'opsHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE'
|
||||
OR "fieldSourceMap"->'opsDeptHead'->>'sourceCode' = 'JST_SKU_INFO_MANAGE'
|
||||
OR "fieldSourceMap"->'designer'->>'sourceCode' = 'JST_SKU_INFO_MANAGE';
|
||||
|
||||
CREATE INDEX ON "_retired_js_sku_info_manage_archives" ("id");
|
||||
|
||||
UPDATE "SupplyChainProductArchive" AS archive
|
||||
SET
|
||||
"opsHead" = CASE WHEN retired."clearOpsHead" THEN '' ELSE archive."opsHead" END,
|
||||
"opsDeptHead" = CASE WHEN retired."clearOpsDeptHead" THEN '' ELSE archive."opsDeptHead" END,
|
||||
"designer" = CASE WHEN retired."clearDesigner" THEN '' ELSE archive."designer" END,
|
||||
"opsDirector" = CASE WHEN retired."clearOpsDirector" THEN '' ELSE archive."opsDirector" END,
|
||||
"fieldSourceMap" = archive."fieldSourceMap" - retired."sourceKeys",
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
FROM "_retired_js_sku_info_manage_archives" AS retired
|
||||
WHERE archive."id" = retired."id";
|
||||
|
||||
DELETE FROM "SupplyChainProductArchiveSource"
|
||||
WHERE "sourceCode" = 'JST_SKU_INFO_MANAGE';
|
||||
|
||||
DELETE FROM "SupplyChainProductArchiveSource" AS source
|
||||
USING "_retired_js_sku_info_manage_archives" AS retired
|
||||
WHERE
|
||||
source."archiveId" = retired."id"
|
||||
AND retired."clearOpsDirector"
|
||||
AND source."sourceCode" = 'SYSTEM_USER_DEPARTMENT';
|
||||
|
||||
COMMIT;
|
||||
@ -67,11 +67,10 @@ async function main() {
|
||||
}
|
||||
|
||||
const syncJstBase = process.argv.includes('--sync-jst-base')
|
||||
const operatorOnly = process.argv.includes('--operator-only')
|
||||
const opsDirectorOnly = process.argv.includes('--ops-director-only')
|
||||
if (operatorOnly && opsDirectorOnly) {
|
||||
throw new Error('--operator-only and --ops-director-only cannot be used together')
|
||||
if (process.argv.includes('--operator-only')) {
|
||||
throw new Error('--operator-only has been retired; js_sku_info_manage is no longer a product archive source.')
|
||||
}
|
||||
const opsDirectorOnly = process.argv.includes('--ops-director-only')
|
||||
const jstBatchSize = Number(readArgument('jst-batch-size') || '500')
|
||||
if (
|
||||
!Number.isSafeInteger(jstBatchSize) ||
|
||||
@ -92,13 +91,11 @@ async function main() {
|
||||
const [
|
||||
{ prisma },
|
||||
{ supplyChainProductArchiveEnrichmentSyncService },
|
||||
{ supplyChainProductArchiveOperatorInitialSyncService },
|
||||
{ supplyChainProductArchiveOpsDirectorSyncService },
|
||||
{ jstSkuProductArchiveFullSyncService },
|
||||
] = await Promise.all([
|
||||
import('../src/lib/prisma'),
|
||||
import('../src/server/services/supply-chain-product-archive-enrichment-sync.service'),
|
||||
import('../src/server/services/supply-chain-product-archive-operator-initial-sync.service'),
|
||||
import('../src/server/services/supply-chain-product-archive-ops-director-sync.service'),
|
||||
syncJstBase
|
||||
? import('../src/server/services/jst-sku-product-archive-full-sync.service')
|
||||
@ -112,24 +109,18 @@ async function main() {
|
||||
concurrency: jstConcurrency,
|
||||
})
|
||||
: null
|
||||
const operatorInitialResult = operatorOnly
|
||||
? await supplyChainProductArchiveOperatorInitialSyncService.syncAll({
|
||||
batchSize: batchSizeValue,
|
||||
})
|
||||
: null
|
||||
const opsDirectorResult = opsDirectorOnly
|
||||
? await supplyChainProductArchiveOpsDirectorSyncService.syncAll({
|
||||
batchSize: batchSizeValue,
|
||||
})
|
||||
: null
|
||||
const enrichmentResult = operatorOnly || opsDirectorOnly
|
||||
const enrichmentResult = opsDirectorOnly
|
||||
? null
|
||||
: await supplyChainProductArchiveEnrichmentSyncService.syncAll({
|
||||
batchSize: batchSizeValue,
|
||||
})
|
||||
console.log(JSON.stringify({
|
||||
baseResult,
|
||||
operatorInitialResult,
|
||||
opsDirectorResult,
|
||||
enrichmentResult,
|
||||
}, null, 2))
|
||||
|
||||
@ -11,7 +11,6 @@ export const JST_WAREHOUSE_ARCHIVE_SOURCE_CODE = 'JST_WAREHOUSE_INFO'
|
||||
export const JST_SKU_PRICE_ARCHIVE_SOURCE_CODE = 'JST_SKU_PRICE'
|
||||
export const PRODUCT_CATALOG_COST_ARCHIVE_SOURCE_CODE = 'PRODUCT_CATALOG_COST_PRICE'
|
||||
export const WAREHOUSE_CONFIG_ARCHIVE_SOURCE_CODE = 'WAREHOUSE_CONFIG'
|
||||
export const JST_SKU_OPERATOR_ARCHIVE_SOURCE_CODE = 'JST_SKU_INFO_MANAGE'
|
||||
export const SYSTEM_USER_DEPARTMENT_ARCHIVE_SOURCE_CODE = 'SYSTEM_USER_DEPARTMENT'
|
||||
|
||||
export type ProductArchiveFieldSource = {
|
||||
@ -80,12 +79,6 @@ export const PRODUCT_DEVELOPMENT_ARCHIVE_FIELD_COLUMNS = {
|
||||
minimumOrderQuantity: 'archive_minimum_order_quantity',
|
||||
} as const
|
||||
|
||||
export const JST_SKU_OPERATOR_ARCHIVE_FIELD_COLUMNS = {
|
||||
opsDeptHead: 'operation_dept_manager',
|
||||
opsHead: 'sku_manager',
|
||||
designer: 'designer',
|
||||
} as const
|
||||
|
||||
export const DRAWING_MEDIA_ARCHIVE_FIELD_COLUMNS = {
|
||||
installDocId: '图纸ID',
|
||||
hasDrawing: '线稿图 | 爆炸图 | 安装图',
|
||||
|
||||
@ -36,36 +36,36 @@ function createTransaction(input: {
|
||||
}
|
||||
}
|
||||
|
||||
const initialSource = {
|
||||
sourceCode: 'JST_SKU_INFO_MANAGE',
|
||||
sourceSystem: '聚水潭 JSReportReplica',
|
||||
sourceSchema: 'rpt',
|
||||
sourceObject: 'js_sku_info_manage',
|
||||
const initialImportSource = {
|
||||
sourceCode: 'INITIAL_OWNER_IMPORT',
|
||||
sourceSystem: 'Local seed import',
|
||||
sourceSchema: 'public',
|
||||
sourceObject: 'ProductArchiveOwnerSeed',
|
||||
sourceRecordId: 'BAR-001',
|
||||
sourceColumns: {
|
||||
designer: 'designer',
|
||||
opsDeptHead: 'operation_dept_manager',
|
||||
opsHead: 'sku_manager',
|
||||
opsDeptHead: 'opsDeptHead',
|
||||
opsHead: 'opsHead',
|
||||
},
|
||||
fillEmptyOnly: true,
|
||||
} as const
|
||||
|
||||
describe('product archive initial operator source', () => {
|
||||
describe('product archive initial import source', () => {
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
|
||||
it('fills only empty owner fields and preserves an existing designer', async () => {
|
||||
const tx = createTransaction({ designer: '现有设计师' })
|
||||
const tx = createTransaction({ designer: 'Existing designer' })
|
||||
mockPrisma.$transaction.mockImplementation(async (callback) => callback(tx))
|
||||
const repository = new SupplyChainProductArchiveEnrichmentSyncRepositoryClass()
|
||||
|
||||
const result = await repository.applyPatch({
|
||||
archiveId: 'archive-1',
|
||||
patch: {
|
||||
designer: '来源设计师',
|
||||
opsDeptHead: '运营部门负责人',
|
||||
opsHead: '运营负责人',
|
||||
designer: 'Imported designer',
|
||||
opsDeptHead: 'Imported department owner',
|
||||
opsHead: 'Imported operator',
|
||||
},
|
||||
source: initialSource,
|
||||
source: initialImportSource,
|
||||
})
|
||||
|
||||
expect(result.changes.map((change) => change.field)).toEqual([
|
||||
@ -76,23 +76,23 @@ describe('product archive initial operator source', () => {
|
||||
expect.objectContaining({
|
||||
where: { id: 'archive-1' },
|
||||
data: expect.objectContaining({
|
||||
opsDeptHead: '运营部门负责人',
|
||||
opsHead: '运营负责人',
|
||||
opsDeptHead: 'Imported department owner',
|
||||
opsHead: 'Imported operator',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(tx.supplyChainProductArchive.update.mock.calls[0][0].data).not.toHaveProperty('designer')
|
||||
})
|
||||
|
||||
it('never reapplies the initial source after that barcode was initialized', async () => {
|
||||
it('never reapplies an initial source after that barcode was initialized', async () => {
|
||||
const tx = createTransaction({ existingSource: { id: 'source-1' } })
|
||||
mockPrisma.$transaction.mockImplementation(async (callback) => callback(tx))
|
||||
const repository = new SupplyChainProductArchiveEnrichmentSyncRepositoryClass()
|
||||
|
||||
await expect(repository.applyPatch({
|
||||
archiveId: 'archive-1',
|
||||
patch: { opsHead: '后续变化的负责人' },
|
||||
source: initialSource,
|
||||
patch: { opsHead: 'Changed operator' },
|
||||
source: initialImportSource,
|
||||
})).resolves.toEqual({ changes: [], sourceInitialized: false })
|
||||
|
||||
expect(tx.supplyChainProductArchive.update).not.toHaveBeenCalled()
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
import sql from 'mssql'
|
||||
|
||||
export type JstSkuOperatorProfile = {
|
||||
barcode: string
|
||||
sku: string
|
||||
skuManager: string
|
||||
operationDeptManager: string
|
||||
designer: string
|
||||
updatedAt: Date | null
|
||||
}
|
||||
|
||||
export interface JstSkuOperatorRepository {
|
||||
listByKeys(keys: readonly { barcode: string; sku: string }[]): Promise<JstSkuOperatorProfile[]>
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
function requiredEnv(name: string) {
|
||||
const value = process.env[name]?.trim()
|
||||
if (!value) throw new Error(`Missing required environment variable: ${name}`)
|
||||
return value
|
||||
}
|
||||
|
||||
function booleanEnv(name: string, fallback: boolean) {
|
||||
const value = process.env[name]?.trim().toLowerCase()
|
||||
if (!value) return fallback
|
||||
return value === 'true' || value === '1'
|
||||
}
|
||||
|
||||
function safeIdentifier(value: string, label: string) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
||||
throw new Error(`${label} is not a valid SQL Server identifier`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function normalizedKeys(keys: readonly { barcode: string; sku: string }[]) {
|
||||
const barcodes = Array.from(
|
||||
new Set(keys.map((key) => key.barcode.trim().toUpperCase()).filter(Boolean)),
|
||||
).slice(0, 200)
|
||||
const skus = Array.from(
|
||||
new Set(keys.map((key) => key.sku.trim().toUpperCase()).filter(Boolean)),
|
||||
).slice(0, 200)
|
||||
return { barcodes, skus }
|
||||
}
|
||||
|
||||
class JstSkuOperatorRepositoryClass implements JstSkuOperatorRepository {
|
||||
private pool: sql.ConnectionPool | null = null
|
||||
|
||||
private async getPool() {
|
||||
if (this.pool?.connected) return this.pool
|
||||
this.pool = await new sql.ConnectionPool({
|
||||
server: requiredEnv('JST_REPORT_DB_HOST'),
|
||||
port: Number(process.env.JST_REPORT_DB_PORT?.trim() || '1433'),
|
||||
database: requiredEnv('JST_REPORT_DB_NAME'),
|
||||
user: requiredEnv('JST_REPORT_DB_USER'),
|
||||
password: requiredEnv('JST_REPORT_DB_PASSWORD'),
|
||||
options: {
|
||||
encrypt: booleanEnv('JST_REPORT_DB_ENCRYPT', true),
|
||||
trustServerCertificate: booleanEnv('JST_REPORT_DB_TRUST_SERVER_CERTIFICATE', true),
|
||||
},
|
||||
pool: { min: 0, max: 4, idleTimeoutMillis: 30_000 },
|
||||
requestTimeout: 30_000,
|
||||
connectionTimeout: 15_000,
|
||||
}).connect()
|
||||
return this.pool
|
||||
}
|
||||
|
||||
async listByKeys(keys: readonly { barcode: string; sku: string }[]) {
|
||||
const normalized = normalizedKeys(keys)
|
||||
if (normalized.barcodes.length === 0 && normalized.skus.length === 0) return []
|
||||
const schema = safeIdentifier(
|
||||
process.env.JST_REPORT_DB_SCHEMA?.trim() || 'rpt',
|
||||
'JST_REPORT_DB_SCHEMA',
|
||||
)
|
||||
const pool = await this.getPool()
|
||||
const request = pool.request()
|
||||
const barcodeParameters = normalized.barcodes.map((barcode, index) => {
|
||||
const parameter = `barcode${index}`
|
||||
request.input(parameter, sql.NVarChar(120), barcode)
|
||||
return `@${parameter}`
|
||||
})
|
||||
const skuParameters = normalized.skus.map((sku, index) => {
|
||||
const parameter = `sku${index}`
|
||||
request.input(parameter, sql.NVarChar(120), sku)
|
||||
return `@${parameter}`
|
||||
})
|
||||
const conditions = [
|
||||
barcodeParameters.length
|
||||
? `UPPER(LTRIM(RTRIM(CAST(sku_id AS nvarchar(120))))) IN (${barcodeParameters.join(', ')})`
|
||||
: '',
|
||||
skuParameters.length
|
||||
? `UPPER(LTRIM(RTRIM(CAST(sku AS nvarchar(120))))) IN (${skuParameters.join(', ')})`
|
||||
: '',
|
||||
].filter(Boolean)
|
||||
const result = await request.query<JstSkuOperatorProfile>(`
|
||||
SELECT
|
||||
UPPER(LTRIM(RTRIM(CAST(sku_id AS nvarchar(120))))) AS barcode,
|
||||
UPPER(LTRIM(RTRIM(CAST(sku AS nvarchar(120))))) AS sku,
|
||||
LTRIM(RTRIM(COALESCE(CAST(sku_manager AS nvarchar(120)), N''))) AS skuManager,
|
||||
LTRIM(RTRIM(COALESCE(CAST(operation_dept_manager AS nvarchar(120)), N''))) AS operationDeptManager,
|
||||
LTRIM(RTRIM(COALESCE(CAST(designer AS nvarchar(120)), N''))) AS designer,
|
||||
update_time AS updatedAt
|
||||
FROM [${schema}].[js_sku_info_manage]
|
||||
WHERE ${conditions.join(' OR ')}
|
||||
`)
|
||||
return result.recordset
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.pool) return
|
||||
await this.pool.close()
|
||||
this.pool = null
|
||||
}
|
||||
}
|
||||
|
||||
export const jstSkuOperatorRepository = new JstSkuOperatorRepositoryClass()
|
||||
@ -1,102 +0,0 @@
|
||||
import {
|
||||
buildProductArchiveEnrichmentUpdates,
|
||||
buildProductArchiveOperatorInitialUpdate,
|
||||
} from '../supply-chain-product-archive-enrichment-sync.mapper'
|
||||
|
||||
const emptyLookups = {
|
||||
suppliers: [],
|
||||
stocks: [],
|
||||
warehouses: [],
|
||||
prices: [],
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
|
||||
describe('product archive operator source mapping', () => {
|
||||
it('matches the source by exact barcode before SKU and marks it as initial-only', () => {
|
||||
const update = buildProductArchiveOperatorInitialUpdate(
|
||||
{
|
||||
id: 'archive-1',
|
||||
barcode: 'BAR-001',
|
||||
spu: 'SPU-001',
|
||||
sku: 'SKU-001',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
},
|
||||
[
|
||||
{
|
||||
barcode: 'BAR-OTHER',
|
||||
sku: 'SKU-001',
|
||||
skuManager: '错误负责人',
|
||||
operationDeptManager: '错误部门负责人',
|
||||
designer: '',
|
||||
updatedAt: null,
|
||||
},
|
||||
{
|
||||
barcode: 'BAR-001',
|
||||
sku: 'SKU-001',
|
||||
skuManager: '王也',
|
||||
operationDeptManager: '子龙',
|
||||
designer: '财神',
|
||||
updatedAt: new Date('2026-07-30T08:01:00.000Z'),
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
expect(update).toEqual(expect.objectContaining({
|
||||
patch: {
|
||||
opsDeptHead: '子龙',
|
||||
opsHead: '王也',
|
||||
designer: '财神',
|
||||
},
|
||||
source: expect.objectContaining({
|
||||
sourceObject: 'js_sku_info_manage',
|
||||
fillEmptyOnly: true,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('uses SKU only when that SKU has one unambiguous source row', () => {
|
||||
const target = {
|
||||
id: 'archive-2',
|
||||
barcode: 'BAR-NOT-MATCHED',
|
||||
spu: 'SPU-002',
|
||||
sku: 'SKU-002',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
}
|
||||
const unique = buildProductArchiveOperatorInitialUpdate(target, [{
|
||||
barcode: 'SOURCE-BAR',
|
||||
sku: 'SKU-002',
|
||||
skuManager: '唯一负责人',
|
||||
operationDeptManager: '唯一部门负责人',
|
||||
designer: '唯一设计师',
|
||||
updatedAt: null,
|
||||
}])
|
||||
const ambiguous = buildProductArchiveOperatorInitialUpdate(target, [
|
||||
{
|
||||
barcode: 'SOURCE-BAR-1',
|
||||
sku: 'SKU-002',
|
||||
skuManager: '负责人一',
|
||||
operationDeptManager: '',
|
||||
designer: '',
|
||||
updatedAt: null,
|
||||
},
|
||||
{
|
||||
barcode: 'SOURCE-BAR-2',
|
||||
sku: 'SKU-002',
|
||||
skuManager: '负责人二',
|
||||
operationDeptManager: '',
|
||||
designer: '',
|
||||
updatedAt: null,
|
||||
},
|
||||
])
|
||||
|
||||
expect(unique?.patch).toEqual({
|
||||
opsDeptHead: '唯一部门负责人',
|
||||
opsHead: '唯一负责人',
|
||||
designer: '唯一设计师',
|
||||
})
|
||||
expect(ambiguous).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -1,13 +1,11 @@
|
||||
import {
|
||||
JST_SKU_PRICE_ARCHIVE_SOURCE_CODE,
|
||||
JST_SKU_OPERATOR_ARCHIVE_SOURCE_CODE,
|
||||
JST_STOCK_MONITOR_ARCHIVE_SOURCE_CODE,
|
||||
JST_SUPPLIER_ARCHIVE_SOURCE_CODE,
|
||||
JST_WAREHOUSE_ARCHIVE_SOURCE_CODE,
|
||||
PRODUCT_CATALOG_COST_ARCHIVE_SOURCE_CODE,
|
||||
WAREHOUSE_CONFIG_ARCHIVE_SOURCE_CODE,
|
||||
} from '@/lib/supply-chain-product-archive-field-source'
|
||||
import type { JstSkuOperatorProfile } from '@/server/repositories/jst-sku-operator.repository'
|
||||
import type { JstSkuPriceProfile } from '@/server/repositories/jst-sku-price.repository'
|
||||
import type { JstStockMonitorProfile } from '@/server/repositories/jst-stock-monitor.repository'
|
||||
import type { JstSupplierProfile } from '@/server/repositories/jst-supplier-profile.repository'
|
||||
@ -93,53 +91,6 @@ function source(
|
||||
}
|
||||
}
|
||||
|
||||
function findOperatorProfile(
|
||||
target: ProductArchiveEnrichmentTarget,
|
||||
profiles: readonly JstSkuOperatorProfile[],
|
||||
) {
|
||||
const barcode = normalizeIdentity(target.barcode)
|
||||
const exact = barcode
|
||||
? profiles.find((profile) => normalizeIdentity(profile.barcode) === barcode)
|
||||
: undefined
|
||||
if (exact) return exact
|
||||
|
||||
const sku = normalizeIdentity(target.sku)
|
||||
if (!sku) return undefined
|
||||
const candidates = profiles.filter(
|
||||
(profile) => normalizeIdentity(profile.sku) === sku,
|
||||
)
|
||||
return candidates.length === 1 ? candidates[0] : undefined
|
||||
}
|
||||
|
||||
export function buildProductArchiveOperatorInitialUpdate(
|
||||
target: ProductArchiveEnrichmentTarget,
|
||||
operators: readonly JstSkuOperatorProfile[],
|
||||
): ProductArchiveEnrichmentUpdate | null {
|
||||
const operator = findOperatorProfile(target, operators)
|
||||
return operator
|
||||
? {
|
||||
patch: {
|
||||
opsDeptHead: operator.operationDeptManager,
|
||||
opsHead: operator.skuManager,
|
||||
designer: operator.designer,
|
||||
},
|
||||
source: source(
|
||||
JST_SKU_OPERATOR_ARCHIVE_SOURCE_CODE,
|
||||
JST_SOURCE_SYSTEM,
|
||||
JST_SOURCE_SCHEMA,
|
||||
'js_sku_info_manage',
|
||||
operator.barcode || operator.sku,
|
||||
{
|
||||
opsDeptHead: 'operation_dept_manager',
|
||||
opsHead: 'sku_manager',
|
||||
designer: 'designer',
|
||||
},
|
||||
{ sourceRecordedAt: operator.updatedAt, fillEmptyOnly: true },
|
||||
),
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
export function buildProductArchiveEnrichmentUpdates(
|
||||
target: ProductArchiveEnrichmentTarget,
|
||||
lookups: ProductArchiveEnrichmentLookups,
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
import {
|
||||
jstSkuOperatorRepository,
|
||||
type JstSkuOperatorRepository,
|
||||
} from '@/server/repositories/jst-sku-operator.repository'
|
||||
import {
|
||||
supplyChainProductArchiveEnrichmentSyncRepository,
|
||||
type SupplyChainProductArchiveEnrichmentSyncRepository,
|
||||
} from '@/server/repositories/supply-chain-product-archive-enrichment-sync.repository'
|
||||
import { recordProductArchiveFieldChanges } from './supply-chain-product-archive-change-log.service'
|
||||
import { buildProductArchiveOperatorInitialUpdate } from './supply-chain-product-archive-enrichment-sync.mapper'
|
||||
|
||||
export type ProductArchiveOperatorInitialSyncResult = {
|
||||
batches: number
|
||||
scanned: number
|
||||
matched: number
|
||||
changed: number
|
||||
unchanged: number
|
||||
lastId: string
|
||||
}
|
||||
|
||||
type OperatorInitialSyncDependencies = {
|
||||
repository: SupplyChainProductArchiveEnrichmentSyncRepository
|
||||
operatorRepository: Pick<JstSkuOperatorRepository, 'listByKeys' | 'close'>
|
||||
}
|
||||
|
||||
export class SupplyChainProductArchiveOperatorInitialSyncService {
|
||||
constructor(
|
||||
private readonly dependencies: OperatorInitialSyncDependencies = {
|
||||
repository: supplyChainProductArchiveEnrichmentSyncRepository,
|
||||
operatorRepository: jstSkuOperatorRepository,
|
||||
},
|
||||
) {}
|
||||
|
||||
async syncAll(input: { batchSize?: number } = {}) {
|
||||
const batchSize = Math.min(200, Math.max(1, Math.trunc(input.batchSize ?? 200)))
|
||||
const result: ProductArchiveOperatorInitialSyncResult = {
|
||||
batches: 0,
|
||||
scanned: 0,
|
||||
matched: 0,
|
||||
changed: 0,
|
||||
unchanged: 0,
|
||||
lastId: '',
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const targets = await this.dependencies.repository.listTargets({
|
||||
afterId: result.lastId || undefined,
|
||||
limit: batchSize,
|
||||
})
|
||||
if (targets.length === 0) break
|
||||
|
||||
const operators = await this.dependencies.operatorRepository.listByKeys(
|
||||
targets.map((target) => ({ barcode: target.barcode, sku: target.sku })),
|
||||
)
|
||||
for (const target of targets) {
|
||||
const update = buildProductArchiveOperatorInitialUpdate(target, operators)
|
||||
if (!update) {
|
||||
result.unchanged += 1
|
||||
continue
|
||||
}
|
||||
result.matched += 1
|
||||
const applied = await this.dependencies.repository.applyPatch({
|
||||
archiveId: target.id,
|
||||
patch: update.patch,
|
||||
source: update.source,
|
||||
})
|
||||
if (applied.changes.length === 0) {
|
||||
result.unchanged += 1
|
||||
continue
|
||||
}
|
||||
result.changed += applied.changes.length
|
||||
if (applied.sourceInitialized) continue
|
||||
await recordProductArchiveFieldChanges({
|
||||
barcode: target.barcode,
|
||||
actor: {
|
||||
source: 'SYSTEM',
|
||||
actorName: `${update.source.sourceSystem} / ${update.source.sourceSchema} / ${update.source.sourceObject}`,
|
||||
},
|
||||
source: {
|
||||
sourceSystem: update.source.sourceSystem,
|
||||
sourceSchema: update.source.sourceSchema,
|
||||
sourceObject: update.source.sourceObject,
|
||||
},
|
||||
changes: applied.changes,
|
||||
})
|
||||
}
|
||||
|
||||
result.batches += 1
|
||||
result.scanned += targets.length
|
||||
result.lastId = targets[targets.length - 1]?.id ?? result.lastId
|
||||
if (targets.length < batchSize) break
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
await this.dependencies.operatorRepository.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const supplyChainProductArchiveOperatorInitialSyncService =
|
||||
new SupplyChainProductArchiveOperatorInitialSyncService()
|
||||
Loading…
x
Reference in New Issue
Block a user