chore: migrate to pnpm workspaces monorepo with Turborepo
- Restructure directories: apps/api, apps/admin, apps/website - Add root pnpm-workspace.yaml, turbo.json, .prettierrc, .gitignore - Rename packages to @inkreach/api, @inkreach/admin, @inkreach/website - Add shared packages: packages/tsconfig, packages/shared-types - Add pnpm.onlyBuiltDependencies for native builds - Update docs: README.md, structs.md - All three projects build successfully
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import type { SyncLog } from '@prisma/client';
|
||||
|
||||
export class SyncLogDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
type!: 'CATEGORIES' | 'PRODUCTS';
|
||||
|
||||
@ApiProperty()
|
||||
status!: 'RUNNING' | 'SUCCESS' | 'FAILED';
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
message!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
startedAt!: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
finishedAt!: string | null;
|
||||
|
||||
static from(log: SyncLog): SyncLogDto {
|
||||
return {
|
||||
id: log.id.toString(),
|
||||
type: log.type,
|
||||
status: log.status,
|
||||
message: log.message,
|
||||
startedAt: log.startedAt.toISOString(),
|
||||
finishedAt: log.finishedAt ? log.finishedAt.toISOString() : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
const POD_HEADERS = {
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
Origin: 'https://inkpod.vip',
|
||||
Referer: 'https://inkpod.vip/',
|
||||
} as const;
|
||||
|
||||
export interface SdsCategoryTreeNode {
|
||||
id: number | string | null;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
children?: SdsCategoryTreeNode[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SdsProduct {
|
||||
id: number | string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
pic?: string;
|
||||
image?: string;
|
||||
psd_img_url?: string;
|
||||
thumbImgUrl?: string;
|
||||
blankDesignUrl?: string;
|
||||
img_url?: string;
|
||||
show_img?: string;
|
||||
price?: number | string;
|
||||
currentPrice?: number | string;
|
||||
categoryId?: number | string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SdsProductsPage {
|
||||
items?: SdsProduct[];
|
||||
content?: SdsProduct[];
|
||||
totalCount?: number;
|
||||
totalElements?: number;
|
||||
total?: number;
|
||||
page?: number;
|
||||
size?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin wrapper around the SDS (mapi.sdspod.com) endpoints that the
|
||||
* `SyncService` consumes.
|
||||
*
|
||||
* Exposed as its own service so it can be mocked cleanly in unit tests.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SdsClientService {
|
||||
private readonly logger = new Logger(SdsClientService.name);
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly http: HttpService,
|
||||
config: ConfigService,
|
||||
) {
|
||||
this.baseUrl =
|
||||
config.get<string>('SDS_API_BASE')?.replace(/\/$/, '') ??
|
||||
'https://mapi.sdspod.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the SDS category tree of type 3 (products category).
|
||||
* Body matches the legacy inkpod client.
|
||||
*/
|
||||
async fetchCategoryTree(): Promise<SdsCategoryTreeNode[]> {
|
||||
const url = `${this.baseUrl}/category/tree/3`;
|
||||
const body = {
|
||||
withActivityArea: true,
|
||||
withPrivate: true,
|
||||
onlyHaveProduct: true,
|
||||
};
|
||||
const { data } = await firstValueFrom(
|
||||
this.http.post<SdsCategoryTreeNode[]>(url, body, { headers: POD_HEADERS }),
|
||||
);
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches one page of products for a given SDS category.
|
||||
*/
|
||||
async fetchProductsPage(
|
||||
categoryId: string | number,
|
||||
page = 1,
|
||||
size = 50,
|
||||
): Promise<SdsProductsPage> {
|
||||
const url = `${this.baseUrl}/products/page`;
|
||||
const { data } = await firstValueFrom(
|
||||
this.http.get<SdsProductsPage>(url, {
|
||||
headers: POD_HEADERS,
|
||||
params: { categoryId, page, size },
|
||||
}),
|
||||
);
|
||||
return data ?? {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Controller,
|
||||
DefaultValuePipe,
|
||||
Get,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiQuery,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { SyncService } from './sync.service';
|
||||
import { SyncLogDto } from './dto/sync-log.dto';
|
||||
|
||||
@ApiTags('sync')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('sync')
|
||||
export class SyncController {
|
||||
constructor(private readonly service: SyncService) {}
|
||||
|
||||
@Post('categories')
|
||||
@ApiOperation({ summary: 'Manually trigger category sync' })
|
||||
syncCategories() {
|
||||
return this.service.syncCategories();
|
||||
}
|
||||
|
||||
@Post('products')
|
||||
@ApiOperation({ summary: 'Manually trigger product sync' })
|
||||
syncProducts() {
|
||||
return this.service.syncProducts();
|
||||
}
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: 'Recent sync log entries' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
async status(
|
||||
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
|
||||
): Promise<SyncLogDto[]> {
|
||||
const logs = await this.service.getStatus(limit);
|
||||
return logs.map((l) => SyncLogDto.from(l));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { SyncController } from './sync.controller';
|
||||
import { SyncService } from './sync.service';
|
||||
import { SdsClientService } from './sds-client.service';
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot(), HttpModule],
|
||||
controllers: [SyncController],
|
||||
providers: [SyncService, SdsClientService],
|
||||
exports: [SyncService, SdsClientService],
|
||||
})
|
||||
export class SyncModule {}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SyncService } from './sync.service';
|
||||
import { SdsClientService } from './sds-client.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('SyncService', () => {
|
||||
let service: SyncService;
|
||||
let sds: jest.Mocked<SdsClientService>;
|
||||
let prisma: PrismaService;
|
||||
const createdSdsCategoryIds: string[] = [];
|
||||
const createdSdsGoodIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
const sdsMock: Partial<SdsClientService> = {
|
||||
fetchCategoryTree: jest.fn(),
|
||||
fetchProductsPage: jest.fn(),
|
||||
};
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot({ isGlobal: true })],
|
||||
providers: [
|
||||
SyncService,
|
||||
{ provide: SdsClientService, useValue: sdsMock },
|
||||
PrismaService,
|
||||
],
|
||||
}).compile();
|
||||
service = moduleRef.get(SyncService);
|
||||
sds = moduleRef.get(SdsClientService) as jest.Mocked<SdsClientService>;
|
||||
prisma = moduleRef.get(PrismaService);
|
||||
await prisma.onModuleInit();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdSdsCategoryIds.length) {
|
||||
await prisma.category.deleteMany({
|
||||
where: { sdsCategoryId: { in: createdSdsCategoryIds } },
|
||||
});
|
||||
}
|
||||
if (createdSdsGoodIds.length) {
|
||||
await prisma.originGood.deleteMany({
|
||||
where: { sdsGoodId: { in: createdSdsGoodIds } },
|
||||
});
|
||||
}
|
||||
await prisma.onModuleDestroy();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('flattenCategoryTree', () => {
|
||||
it('flattens a nested SDS tree and preserves parent linkage', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Root',
|
||||
children: [
|
||||
{ id: 11, name: 'Child A' },
|
||||
{ id: 12, name: 'Child B', children: [{ id: 121, name: 'Leaf' }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
const flat = service.flattenCategoryTree(tree);
|
||||
expect(flat).toHaveLength(4);
|
||||
const byId = Object.fromEntries(flat.map((n) => [n.sdsId, n]));
|
||||
expect(byId['1'].name).toBe('Root');
|
||||
expect(byId['1'].parentSdsId).toBeUndefined();
|
||||
expect(byId['11'].parentSdsId).toBe('1');
|
||||
expect(byId['12'].parentSdsId).toBe('1');
|
||||
expect(byId['121'].parentSdsId).toBe('12');
|
||||
});
|
||||
|
||||
it('skips nodes without a usable id', () => {
|
||||
const flat = service.flattenCategoryTree([{ id: null, name: 'no-id' }]);
|
||||
expect(flat).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncCategories', () => {
|
||||
it('inserts + updates rows and links parents', async () => {
|
||||
const stamp = Date.now();
|
||||
sds.fetchCategoryTree.mockResolvedValueOnce([
|
||||
{
|
||||
id: `r-${stamp}`,
|
||||
name: `Root ${stamp}`,
|
||||
children: [{ id: `c-${stamp}`, name: `Child ${stamp}` }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.syncCategories();
|
||||
expect(result.total).toBe(2);
|
||||
expect(result.inserted).toBe(2);
|
||||
expect(result.updated).toBe(0);
|
||||
createdSdsCategoryIds.push(`r-${stamp}`, `c-${stamp}`);
|
||||
|
||||
const child = await prisma.category.findUnique({
|
||||
where: { sdsCategoryId: `c-${stamp}` },
|
||||
});
|
||||
const root = await prisma.category.findUnique({
|
||||
where: { sdsCategoryId: `r-${stamp}` },
|
||||
});
|
||||
expect(child?.parentCategoryId).toBe(root?.id);
|
||||
});
|
||||
|
||||
it('marks SyncLog SUCCESS', async () => {
|
||||
const logs = await prisma.syncLog.findMany({
|
||||
where: { type: 'CATEGORIES' },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take: 1,
|
||||
});
|
||||
expect(logs[0]?.status).toBe('SUCCESS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncProducts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('upserts origin goods by sdsGoodId and updates on re-run', async () => {
|
||||
// Seed a leaf category with sdsCategoryId so the sync has work.
|
||||
const stamp = Date.now();
|
||||
const leaf = await prisma.category.create({
|
||||
data: {
|
||||
sdsCategoryId: `leaf-${stamp}`,
|
||||
categoryName: `Leaf ${stamp}`,
|
||||
},
|
||||
});
|
||||
createdSdsCategoryIds.push(`leaf-${stamp}`);
|
||||
|
||||
// Default mock returns a single page with 2 products, then
|
||||
// breaks the loop because content.length < 50.
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leaf-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `p-${stamp}-1`, name: 'Product 1', price: 12.5, pic: 'http://x' },
|
||||
{ id: `p-${stamp}-2`, name: 'Product 2', price: '99.00' },
|
||||
],
|
||||
};
|
||||
}
|
||||
// For any other (already-existing) category, return empty
|
||||
// so the loop terminates immediately.
|
||||
return { content: [] };
|
||||
});
|
||||
|
||||
const result1 = await service.syncProducts();
|
||||
expect(result1.inserted).toBeGreaterThanOrEqual(2);
|
||||
createdSdsGoodIds.push(`p-${stamp}-1`, `p-${stamp}-2`);
|
||||
|
||||
// Re-run with updated name -> should be `updated`, not `inserted`.
|
||||
sds.fetchProductsPage.mockImplementation(async (categoryId) => {
|
||||
if (categoryId === `leaf-${stamp}`) {
|
||||
return {
|
||||
content: [
|
||||
{ id: `p-${stamp}-1`, name: 'Product 1 renamed' },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { content: [] };
|
||||
});
|
||||
const result2 = await service.syncProducts();
|
||||
expect(result2.updated).toBeGreaterThanOrEqual(1);
|
||||
const row = await prisma.originGood.findUnique({
|
||||
where: { sdsGoodId: `p-${stamp}-1` },
|
||||
});
|
||||
expect(row?.goodName).toBe('Product 1 renamed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('returns recent logs ordered by startedAt desc', async () => {
|
||||
const logs = await service.getStatus(5);
|
||||
expect(Array.isArray(logs)).toBe(true);
|
||||
expect(logs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { SdsClientService, SdsCategoryTreeNode, SdsProduct } from './sds-client.service';
|
||||
|
||||
export interface CategorySyncResult {
|
||||
inserted: number;
|
||||
updated: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ProductSyncResult {
|
||||
inserted: number;
|
||||
updated: number;
|
||||
total: number;
|
||||
leafCategories: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SyncService {
|
||||
private readonly logger = new Logger(SyncService.name);
|
||||
private running = { categories: false, products: false };
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly sds: SdsClientService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Hourly full sync — runs `syncCategories` first (since product
|
||||
* sync depends on knowing which leaf categories exist) and then
|
||||
* `syncProducts`.
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_HOUR)
|
||||
async hourlyCron(): Promise<void> {
|
||||
try {
|
||||
await this.syncCategories();
|
||||
await this.syncProducts();
|
||||
} catch (err) {
|
||||
this.logger.error('Hourly cron sync failed', err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async syncCategories(): Promise<CategorySyncResult> {
|
||||
if (this.running.categories) {
|
||||
throw new Error('Category sync already in progress');
|
||||
}
|
||||
this.running.categories = true;
|
||||
const log = await this.prisma.syncLog.create({
|
||||
data: { type: 'CATEGORIES', status: 'RUNNING' },
|
||||
});
|
||||
try {
|
||||
const tree = await this.sds.fetchCategoryTree();
|
||||
const flat = this.flattenCategoryTree(tree);
|
||||
this.logger.log(`Fetched ${flat.length} SDS categories`);
|
||||
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
for (const node of flat) {
|
||||
const existing = await this.prisma.category.findUnique({
|
||||
where: { sdsCategoryId: node.sdsId },
|
||||
});
|
||||
if (!existing) {
|
||||
await this.prisma.category.create({
|
||||
data: {
|
||||
sdsCategoryId: node.sdsId,
|
||||
categoryName: node.name,
|
||||
categoryIcon: node.icon ?? null,
|
||||
},
|
||||
});
|
||||
inserted++;
|
||||
} else {
|
||||
await this.prisma.category.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
categoryName: node.name,
|
||||
categoryIcon: node.icon ?? null,
|
||||
},
|
||||
});
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: wire up parents by sdsCategoryId.
|
||||
for (const node of flat) {
|
||||
if (!node.parentSdsId) continue;
|
||||
const child = await this.prisma.category.findUnique({
|
||||
where: { sdsCategoryId: node.sdsId },
|
||||
});
|
||||
const parent = await this.prisma.category.findUnique({
|
||||
where: { sdsCategoryId: node.parentSdsId },
|
||||
});
|
||||
if (child && parent && child.parentCategoryId !== parent.id) {
|
||||
await this.prisma.category.update({
|
||||
where: { id: child.id },
|
||||
data: { parentCategoryId: parent.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.syncLog.update({
|
||||
where: { id: log.id },
|
||||
data: {
|
||||
status: 'SUCCESS',
|
||||
finishedAt: new Date(),
|
||||
message: `inserted=${inserted} updated=${updated} total=${flat.length}`,
|
||||
},
|
||||
});
|
||||
return { inserted, updated, total: flat.length };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.prisma.syncLog.update({
|
||||
where: { id: log.id },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
finishedAt: new Date(),
|
||||
message,
|
||||
},
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
this.running.categories = false;
|
||||
}
|
||||
}
|
||||
|
||||
async syncProducts(): Promise<ProductSyncResult> {
|
||||
if (this.running.products) {
|
||||
throw new Error('Product sync already in progress');
|
||||
}
|
||||
this.running.products = true;
|
||||
const log = await this.prisma.syncLog.create({
|
||||
data: { type: 'PRODUCTS', status: 'RUNNING' },
|
||||
});
|
||||
try {
|
||||
// Identify leaf categories — those with no children.
|
||||
const all = await this.prisma.category.findMany({
|
||||
select: { id: true, sdsCategoryId: true },
|
||||
});
|
||||
const parents = await this.prisma.category.findMany({
|
||||
where: { parent: { isNot: null } },
|
||||
select: { parentCategoryId: true },
|
||||
});
|
||||
const parentIds = new Set(parents.map((p) => p.parentCategoryId!));
|
||||
const leafRows = all.filter((c) => !parentIds.has(c.id) && c.sdsCategoryId);
|
||||
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
let total = 0;
|
||||
|
||||
for (const leaf of leafRows) {
|
||||
const sdsCategoryId = leaf.sdsCategoryId!;
|
||||
let page = 1;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const resp = await this.sds.fetchProductsPage(sdsCategoryId, page, 50);
|
||||
const products = resp.items ?? resp.content ?? [];
|
||||
if (products.length === 0) break;
|
||||
for (const product of products) {
|
||||
const upserted = await this.upsertOriginGood(product, sdsCategoryId);
|
||||
if (upserted === 'inserted') inserted++;
|
||||
else updated++;
|
||||
total++;
|
||||
}
|
||||
if (products.length < 50) break;
|
||||
page++;
|
||||
if (page > 200) {
|
||||
// Safety net — at most 10k products per category.
|
||||
this.logger.warn(`Reached 200-page safety cap for ${sdsCategoryId}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.syncLog.update({
|
||||
where: { id: log.id },
|
||||
data: {
|
||||
status: 'SUCCESS',
|
||||
finishedAt: new Date(),
|
||||
message: `inserted=${inserted} updated=${updated} total=${total} leafCategories=${leafRows.length}`,
|
||||
},
|
||||
});
|
||||
return { inserted, updated, total, leafCategories: leafRows.length };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await this.prisma.syncLog.update({
|
||||
where: { id: log.id },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
finishedAt: new Date(),
|
||||
message,
|
||||
},
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
this.running.products = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getStatus(limit = 20) {
|
||||
return this.prisma.syncLog.findMany({
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the SDS nested tree into a list of `{ sdsId, parentSdsId?, name, icon? }`.
|
||||
*/
|
||||
flattenCategoryTree(
|
||||
nodes: SdsCategoryTreeNode[],
|
||||
parentSdsId?: string,
|
||||
): Array<{ sdsId: string; parentSdsId?: string; name: string; icon?: string }> {
|
||||
const out: Array<{ sdsId: string; parentSdsId?: string; name: string; icon?: string }> = [];
|
||||
const walk = (node: SdsCategoryTreeNode, parent?: string) => {
|
||||
const sdsId = String(node.id);
|
||||
if (sdsId === '' || sdsId === 'undefined' || sdsId === 'null') return;
|
||||
out.push({
|
||||
sdsId,
|
||||
parentSdsId: parent,
|
||||
name: String(node.name ?? node.title ?? sdsId),
|
||||
icon: node.icon ? String(node.icon) : undefined,
|
||||
});
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) walk(child, sdsId);
|
||||
}
|
||||
};
|
||||
for (const root of nodes) walk(root, parentSdsId);
|
||||
return out;
|
||||
}
|
||||
|
||||
private async upsertOriginGood(
|
||||
product: SdsProduct,
|
||||
sdsCategoryId: string,
|
||||
): Promise<'inserted' | 'updated'> {
|
||||
const sdsGoodId = String(product.id);
|
||||
const existing = await this.prisma.originGood.findUnique({
|
||||
where: { sdsGoodId },
|
||||
});
|
||||
const goodName = String(product.name ?? product.title ?? sdsGoodId);
|
||||
const goodImage = product.psd_img_url
|
||||
? String(product.psd_img_url)
|
||||
: product.blankDesignUrl
|
||||
? String(product.blankDesignUrl)
|
||||
: product.thumbImgUrl
|
||||
? String(product.thumbImgUrl)
|
||||
: product.show_img
|
||||
? String(product.show_img)
|
||||
: product.img_url
|
||||
? String(product.img_url)
|
||||
: product.pic
|
||||
? String(product.pic)
|
||||
: product.image
|
||||
? String(product.image)
|
||||
: null;
|
||||
const priceValue = product.currentPrice ?? product.price;
|
||||
let goodPrice: Prisma.Decimal | null = null;
|
||||
if (priceValue !== undefined && priceValue !== null) {
|
||||
const n = typeof priceValue === 'string' ? Number(priceValue) : priceValue;
|
||||
if (Number.isFinite(n)) {
|
||||
goodPrice = new Prisma.Decimal(n);
|
||||
}
|
||||
}
|
||||
const data: Prisma.OriginGoodUncheckedUpdateInput = {
|
||||
sdsCategoryId,
|
||||
goodName,
|
||||
goodImage,
|
||||
goodPrice,
|
||||
};
|
||||
|
||||
if (!existing) {
|
||||
await this.prisma.originGood.create({
|
||||
data: {
|
||||
sdsGoodId,
|
||||
sdsCategoryId,
|
||||
goodName,
|
||||
goodImage,
|
||||
goodPrice,
|
||||
},
|
||||
});
|
||||
return 'inserted';
|
||||
}
|
||||
await this.prisma.originGood.update({
|
||||
where: { id: existing.id },
|
||||
data,
|
||||
});
|
||||
return 'updated';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user