221 lines
5.9 KiB
JavaScript
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,
|
|
}
|