120 lines
4.2 KiB
TypeScript
120 lines
4.2 KiB
TypeScript
import logger from '@adonisjs/core/services/logger'
|
|
import ProductService from '#services/product_service'
|
|
import LogService from '#services/log_service'
|
|
import ErpService, { type ErpProductItem } from '#services/erp_service'
|
|
import { enqueueProductUpserts } from '#services/queue_service'
|
|
|
|
export interface SyncSummary {
|
|
/** Tổng số bản ghi ERP tự báo cáo (field `total`) — CHỈ tham khảo, không tin cậy. */
|
|
total: number
|
|
/** Số item fetch về từ ERP. */
|
|
fetched: number
|
|
/** Số item hợp lệ đã đẩy lên queue. */
|
|
enqueued: number
|
|
/** Số item bị bỏ qua (sku rỗng / rác). */
|
|
skipped: number
|
|
/** Số lần gọi ERP (số page). */
|
|
pages: number
|
|
startedAt: string
|
|
finishedAt?: string
|
|
}
|
|
|
|
interface SyncOptions {
|
|
/** Bước phân trang (giá trị `skip` tăng mỗi lần). Mặc định 100. */
|
|
pageSize?: number
|
|
/** Trần số trang để chặn lặp vô hạn nếu ERP không bao giờ trả rỗng. Mặc định 1000. */
|
|
maxPages?: number
|
|
}
|
|
|
|
/**
|
|
* Service đồng bộ sản phẩm từ ERP qua BullMQ.
|
|
*
|
|
* Luồng:
|
|
* 1. `syncFromErp` (chạy trong job `erp`) — orchestrator: quét toàn bộ ERP
|
|
* theo phân trang và fan-out mỗi sản phẩm thành 1 job `upsert`.
|
|
* 2. Worker xử lý job `upsert` gọi `upsertProduct`. Nếu lỗi, BullMQ tự retry
|
|
* theo `attempts` + exponential backoff -> sản phẩm lỗi được sync lại sau
|
|
* cùng (sau backoff), không cần hàng đợi retry thủ công.
|
|
*
|
|
* LƯU Ý PHÂN TRANG (ERP /api/products/instock):
|
|
* - ERP BỎ QUA `limit` (trả số item/trang tùy ý, vd 195/309), và `total` báo
|
|
* về KHÔNG khớp số bản ghi thật -> KHÔNG dùng `total` làm điều kiện dừng.
|
|
* - `skip` hoạt động như cursor bước `pageSize`: skip=0 và skip=100 cho 2 block
|
|
* RỜI NHAU. Vì vậy tăng `skip += pageSize` và lặp tới khi trang trả về RỖNG.
|
|
*/
|
|
export default class SyncService {
|
|
static async syncFromErp(username = 'system', options: SyncOptions = {}): Promise<SyncSummary> {
|
|
const pageSize = options.pageSize ?? 100
|
|
const maxPages = options.maxPages ?? 1000
|
|
|
|
const summary: SyncSummary = {
|
|
total: 0,
|
|
fetched: 0,
|
|
enqueued: 0,
|
|
skipped: 0,
|
|
pages: 0,
|
|
startedAt: new Date().toISOString(),
|
|
}
|
|
|
|
let skip = 0
|
|
|
|
while (summary.pages < maxPages) {
|
|
const page = await ErpService.getProductsForSync({
|
|
limit: pageSize,
|
|
skip,
|
|
order: '',
|
|
where: { sku: '', warehouse: '', condition: '' },
|
|
})
|
|
|
|
summary.pages++
|
|
if (Number.isFinite(page.total)) summary.total = page.total
|
|
|
|
// Điều kiện dừng TIN CẬY: ERP hết dữ liệu (trả trang rỗng).
|
|
if (page.items.length === 0) break
|
|
|
|
summary.fetched += page.items.length
|
|
|
|
// Bỏ qua item rác không có sku (sku rỗng gây trùng unique key -> lỗi insert).
|
|
const valid = page.items.filter((i) => i.sku && i.sku.trim() !== '')
|
|
summary.skipped += page.items.length - valid.length
|
|
|
|
if (valid.length > 0) {
|
|
// Fan-out: mỗi sản phẩm 1 job upsert. BullMQ lo retry khi lỗi.
|
|
await enqueueProductUpserts(valid, username)
|
|
summary.enqueued += valid.length
|
|
}
|
|
|
|
logger.info(
|
|
{ skip, fetched: page.items.length, valid: valid.length, reportedTotal: page.total },
|
|
'ERP page đã enqueue'
|
|
)
|
|
skip += pageSize
|
|
}
|
|
|
|
if (summary.pages >= maxPages) {
|
|
logger.warn({ maxPages }, 'Sync ERP chạm trần maxPages — có thể chưa quét hết, kiểm tra lại')
|
|
}
|
|
|
|
summary.finishedAt = new Date().toISOString()
|
|
|
|
await LogService.record({
|
|
username,
|
|
actionName: 'Đồng bộ ERP',
|
|
action: 'sync',
|
|
meta: summary,
|
|
})
|
|
|
|
logger.info(summary, 'Sync ERP: đã enqueue toàn bộ sản phẩm')
|
|
return summary
|
|
}
|
|
|
|
/**
|
|
* Xử lý 1 job upsert (gọi bởi worker).
|
|
* Cố tình ném lỗi khi thất bại để BullMQ tự retry job.
|
|
*/
|
|
static async upsertProduct(item: ErpProductItem): Promise<{ sku: string; condition: string; created: boolean }> {
|
|
const { created } = await ProductService.upsert(item)
|
|
return { sku: item.sku, condition: item.condition, created }
|
|
}
|
|
}
|