HotTRDealsBackend/services/seller.service.js
2026-02-09 21:47:55 +00:00

221 lines
5.9 KiB
JavaScript

// services/seller/sellerService.js
const { findSeller, findSellers } = require("../db/seller.db")
const dealService = require("./deal.service")
const { listSellersFromRedis, setSellerInRedis, setSellersInRedis } = require("./redis/sellerCache.service")
const { getRecentDealIdsBySeller, getSellerDealIndexCount } = require("./redis/sellerDealIndex.service")
const { getDealsByIdsFromRedis } = require("./redis/hotDealList.service")
const DEFAULT_LIMIT = 10
const MAX_LIMIT = 50
function normalizeSellerName(value) {
return String(value || "").trim()
}
function normalizeSeller(seller = {}) {
const id = Number(seller.id)
if (!Number.isInteger(id) || id <= 0) return null
return {
id,
name: String(seller.name || "").trim(),
url: seller.url ?? null,
sellerLogo: seller.sellerLogo ?? null,
isActive: seller.isActive !== undefined ? Boolean(seller.isActive) : true,
}
}
async function listSellersCached() {
let sellers = await listSellersFromRedis()
if (sellers.length) return sellers
sellers = await findSellers(
{},
{ select: { id: true, name: true, url: true, sellerLogo: true, isActive: true }, orderBy: { name: "asc" } }
)
if (sellers.length) {
await setSellersInRedis(sellers)
}
return sellers
}
function clampPagination({ page, limit }) {
const rawPage = Number(page)
const rawLimit = Number(limit)
const normalizedPage = Number.isFinite(rawPage) ? Math.max(1, Math.floor(rawPage)) : 1
let normalizedLimit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : DEFAULT_LIMIT
normalizedLimit = Math.min(MAX_LIMIT, normalizedLimit)
const skip = (normalizedPage - 1) * normalizedLimit
return { page: normalizedPage, limit: normalizedLimit, skip }
}
function normalizeDealCardFromRedis(deal = {}) {
return {
...deal,
id: Number(deal.id),
score: Number.isFinite(deal.score) ? deal.score : 0,
commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0,
price: deal.price ?? null,
originalPrice: deal.originalPrice ?? null,
shippingPrice: deal.shippingPrice ?? null,
discountValue: deal.discountValue ?? null,
}
}
function hasSellerFilters(filters = {}) {
if (!filters || typeof filters !== "object") return false
const keys = [
"status",
"categoryId",
"categoryIds",
"saleType",
"affiliateType",
"minPrice",
"maxPrice",
"priceMin",
"priceMax",
"minScore",
"maxScore",
"sortBy",
"sortDir",
"createdAfter",
"createdBefore",
"from",
"to",
"hasImage",
]
return keys.some((key) => {
const value = filters[key]
return value !== undefined && value !== null && String(value).trim() !== ""
})
}
async function getSellerByName(name) {
const normalized = normalizeSellerName(name)
if (!normalized) {
const err = new Error("SELLER_NAME_REQUIRED")
err.statusCode = 400
throw err
}
const sellers = await listSellersCached()
const lower = normalized.toLowerCase()
const cached = sellers
.map(normalizeSeller)
.filter(Boolean)
.find((seller) => seller.name.toLowerCase() === lower)
if (cached) {
return { id: cached.id, name: cached.name, url: cached.url, sellerLogo: cached.sellerLogo }
}
const seller = await findSeller(
{ name: { equals: normalized, mode: "insensitive" } },
{ select: { id: true, name: true, url: true, sellerLogo: true, isActive: true } }
)
if (seller) {
await setSellerInRedis(seller)
return { id: seller.id, name: seller.name, url: seller.url, sellerLogo: seller.sellerLogo }
}
return null
}
async function getDealsBySellerName(name, { page = 1, limit = 10, filters = {}, viewer = null, scope = "USER" } = {}) {
const seller = await getSellerByName(name)
if (!seller) {
const err = new Error("SELLER_NOT_FOUND")
err.statusCode = 404
throw err
}
const searchTerm = String(filters?.q || "").trim()
const useSellerIndex = !searchTerm && !hasSellerFilters(filters)
if (useSellerIndex) {
const pagination = clampPagination({ page, limit })
const [total, ids] = await Promise.all([
getSellerDealIndexCount(seller.id),
getRecentDealIdsBySeller({
sellerId: seller.id,
offset: pagination.skip,
limit: pagination.limit,
}),
])
if (!total) {
return {
seller,
payload: {
page: pagination.page,
total: 0,
totalPages: 0,
results: [],
},
}
}
if (!ids.length) {
return {
seller,
payload: {
page: pagination.page,
total,
totalPages: Math.ceil(total / pagination.limit),
results: [],
},
}
}
const viewerId = viewer?.userId ? Number(viewer.userId) : null
const deals = await getDealsByIdsFromRedis(ids, viewerId)
if (deals.length === ids.length) {
const activeDeals = deals.filter((deal) => String(deal?.status || "").toUpperCase() === "ACTIVE")
if (activeDeals.length === ids.length) {
return {
seller,
payload: {
page: pagination.page,
total,
totalPages: Math.ceil(total / pagination.limit),
results: activeDeals.map(normalizeDealCardFromRedis),
},
}
}
}
}
const payload = await dealService.getDeals({
preset: "NEW",
q: searchTerm || undefined,
page,
limit,
viewer,
scope,
baseWhere: { sellerId: seller.id, status: "ACTIVE" },
filters,
useRedisSearch: true,
})
return { seller, payload }
}
async function getActiveSellers() {
const sellers = await listSellersCached()
return sellers
.map(normalizeSeller)
.filter((seller) => seller && seller.isActive)
.sort((a, b) => a.name.localeCompare(b.name))
.map((seller) => ({
name: seller.name,
sellerLogo: seller.sellerLogo,
}))
}
module.exports = {
getSellerByName,
getDealsBySellerName,
getActiveSellers,
}