feat(admin): manage and sync product details
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
@@ -67,7 +67,7 @@ export function shouldRunDelistDetection(leafCategories: number, seenGoods: numb
|
||||
@Injectable()
|
||||
export class SyncService {
|
||||
private readonly logger = new Logger(SyncService.name);
|
||||
private running = { categories: false, products: false };
|
||||
private running = { categories: false, products: false, details: false };
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@@ -110,8 +110,18 @@ export class SyncService {
|
||||
return { message: 'Product sync started' };
|
||||
}
|
||||
|
||||
async startProductDetailSync(): Promise<{ message: string }> {
|
||||
if (this.running.details) {
|
||||
return { message: 'Product detail sync already in progress' };
|
||||
}
|
||||
void this.syncProductDetails().catch((err) =>
|
||||
this.logger.error('Product detail sync failed', err as Error),
|
||||
);
|
||||
return { message: 'Product detail sync started' };
|
||||
}
|
||||
|
||||
/** Check if a sync type is currently running. */
|
||||
isRunning(type: 'categories' | 'products'): boolean {
|
||||
isRunning(type: 'categories' | 'products' | 'details'): boolean {
|
||||
return this.running[type];
|
||||
}
|
||||
|
||||
@@ -392,6 +402,66 @@ export class SyncService {
|
||||
});
|
||||
}
|
||||
|
||||
async syncProductDetails(): Promise<{ synced: number; failed: number }> {
|
||||
if (this.running.details) {
|
||||
throw new Error('Product detail sync already in progress');
|
||||
}
|
||||
this.running.details = true;
|
||||
const log = await this.prisma.syncLog.create({
|
||||
data: { type: 'PRODUCT_DETAILS', status: 'RUNNING' },
|
||||
});
|
||||
try {
|
||||
const result = await this.syncConfiguredProductDetails();
|
||||
await this.prisma.syncLog.update({
|
||||
where: { id: log.id },
|
||||
data: {
|
||||
status: 'SUCCESS',
|
||||
finishedAt: new Date(),
|
||||
message: `synced=${result.synced} failed=${result.failed}`,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await this.prisma.syncLog.update({
|
||||
where: { id: log.id },
|
||||
data: { status: 'FAILED', finishedAt: new Date(), message },
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
this.running.details = false;
|
||||
}
|
||||
}
|
||||
|
||||
async syncOneProductDetail(goodId: string): Promise<{
|
||||
goodId: string;
|
||||
variants: number;
|
||||
detailSyncedAt: string;
|
||||
}> {
|
||||
const originGood = await this.prisma.originGood.findUnique({
|
||||
where: { sdsGoodId: goodId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!originGood) {
|
||||
throw new NotFoundException(`SDS product ${goodId} not found locally`);
|
||||
}
|
||||
const upstream = await this.sds.fetchProductDetail(goodId);
|
||||
const normalized = normalizeProductDetail(upstream);
|
||||
await this.persistProductDetail(originGood.id, upstream);
|
||||
return {
|
||||
goodId,
|
||||
variants: normalized.variants.length,
|
||||
detailSyncedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
queueProductDetailSync(goodId: string): void {
|
||||
void this.syncOneProductDetail(goodId).catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.warn(`Queued detail sync failed for ${goodId}: ${message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async syncConfiguredProductDetails(): Promise<{ synced: number; failed: number }> {
|
||||
const configured = await this.prisma.originGood.findMany({
|
||||
where: { delisted: false, goods: { some: {} } },
|
||||
|
||||
Reference in New Issue
Block a user