Use id+slug paths for user products and count their visits.
Align storefront/sitemap URLs with catalog products, add by-id public APIs, and include user-product detail views in the product visit chart. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
3aef62c1b8
commit
6517877bed
@@ -32,14 +32,12 @@ import {
|
||||
} from './dto/user-product.dto';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
import { SitemapService } from '../sitemap/sitemap.service';
|
||||
import { slugifyForUrl } from '../sitemap/seo-slug.util';
|
||||
|
||||
function slugify(value: string): string {
|
||||
return (
|
||||
value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '') || 'user-product'
|
||||
function buildUserProductSlug(titleFa: string, titleEn?: string | null): string {
|
||||
return slugifyForUrl(
|
||||
titleFa.trim() || titleEn?.trim() || '',
|
||||
'user-product',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,11 +166,9 @@ export class UserProductsService {
|
||||
|
||||
async getPublicBySlug(host: string, slug: string) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
const businessId = business.id;
|
||||
|
||||
const product = await this.prisma.userProduct.findFirst({
|
||||
where: {
|
||||
businessId,
|
||||
businessId: business.id,
|
||||
slug,
|
||||
status: ContentStatus.published,
|
||||
},
|
||||
@@ -183,6 +179,81 @@ export class UserProductsService {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
return this.serializePublicDetail(business.id, product);
|
||||
}
|
||||
|
||||
async getPublicById(host: string, productIdRaw: string) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
let productId: bigint;
|
||||
try {
|
||||
productId = BigInt(productIdRaw);
|
||||
} catch {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
const product = await this.prisma.userProduct.findFirst({
|
||||
where: {
|
||||
businessId: business.id,
|
||||
id: productId,
|
||||
status: ContentStatus.published,
|
||||
},
|
||||
include: userProductDetailInclude,
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
return this.serializePublicDetail(business.id, product);
|
||||
}
|
||||
|
||||
async getPublicTechnicalInfoBySlug(host: string, slug: string) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
const product = await this.prisma.userProduct.findFirst({
|
||||
where: {
|
||||
businessId: business.id,
|
||||
slug,
|
||||
status: ContentStatus.published,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
return this.getPublicTechnicalInfoForProduct(business.id, product.id);
|
||||
}
|
||||
|
||||
async getPublicTechnicalInfoById(host: string, productIdRaw: string) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
let productId: bigint;
|
||||
try {
|
||||
productId = BigInt(productIdRaw);
|
||||
} catch {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
const product = await this.prisma.userProduct.findFirst({
|
||||
where: {
|
||||
businessId: business.id,
|
||||
id: productId,
|
||||
status: ContentStatus.published,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
return this.getPublicTechnicalInfoForProduct(business.id, product.id);
|
||||
}
|
||||
|
||||
private async serializePublicDetail(
|
||||
businessId: bigint,
|
||||
product: UserProductDetailRow,
|
||||
) {
|
||||
const categoryByEntity = await this.loadCategoriesForProducts(businessId, [
|
||||
product.id,
|
||||
]);
|
||||
@@ -197,27 +268,14 @@ export class UserProductsService {
|
||||
};
|
||||
}
|
||||
|
||||
async getPublicTechnicalInfoBySlug(host: string, slug: string) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(host);
|
||||
const businessId = business.id;
|
||||
|
||||
const product = await this.prisma.userProduct.findFirst({
|
||||
where: {
|
||||
businessId,
|
||||
slug,
|
||||
status: ContentStatus.published,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
private async getPublicTechnicalInfoForProduct(
|
||||
businessId: bigint,
|
||||
productId: bigint,
|
||||
) {
|
||||
const categoryByEntity = await this.loadCategoriesForProducts(businessId, [
|
||||
product.id,
|
||||
productId,
|
||||
]);
|
||||
const category = categoryByEntity.get(product.id.toString());
|
||||
const category = categoryByEntity.get(productId.toString());
|
||||
if (!category) {
|
||||
return {
|
||||
form: null,
|
||||
@@ -235,7 +293,7 @@ export class UserProductsService {
|
||||
}
|
||||
|
||||
const detail = await this.prisma.userProduct.findFirst({
|
||||
where: { id: product.id },
|
||||
where: { id: productId },
|
||||
include: userProductDetailInclude,
|
||||
});
|
||||
if (!detail) {
|
||||
@@ -337,7 +395,10 @@ export class UserProductsService {
|
||||
const technicalValues = dto.technicalValues ?? [];
|
||||
this.validateTechnicalValues(form?.fields ?? [], technicalValues);
|
||||
|
||||
const slug = await this.ensureUniqueSlug(businessId, slugify(titleFa));
|
||||
const slug = await this.ensureUniqueSlug(
|
||||
businessId,
|
||||
buildUserProductSlug(titleFa, dto.titleEn),
|
||||
);
|
||||
const priceCurrency = dto.priceCurrency ?? 'IRT';
|
||||
const content: Prisma.InputJsonValue = {
|
||||
...(dto.titleEn?.trim() ? { titleEn: dto.titleEn.trim() } : {}),
|
||||
@@ -542,7 +603,7 @@ export class UserProductsService {
|
||||
if (titleFa !== existing.title) {
|
||||
slug = await this.ensureUniqueSlug(
|
||||
businessId,
|
||||
slugify(titleFa),
|
||||
buildUserProductSlug(titleFa, dto.titleEn),
|
||||
productId,
|
||||
);
|
||||
}
|
||||
@@ -1272,6 +1333,7 @@ export class UserProductsService {
|
||||
return {
|
||||
id: product.id.toString(),
|
||||
slug: product.slug,
|
||||
pathSlug: buildUserProductSlug(product.title, titleEn),
|
||||
title: product.title,
|
||||
titleFa: product.title,
|
||||
titleEn,
|
||||
|
||||
Reference in New Issue
Block a user