ca4bef959f
Adjust all entity types to match actual backend response shapes. String-typed numeric fields (Shop.rating, Shop.commissionValue, WalletTransaction.amount) now correctly typed as strings. Post.linkedOrderId changed from SnowflakeId to number (backend int64). Removed fields absent from backend: Order consumer/player/shopName, Review fromUserAvatar/toUserId, Dispute initiatorId/initiatorName, Post authorRole/quotedPostId, Comment postId, ChatSession readonly/lastMessageAt, ChatMessage senderName/senderAvatar. Added Player.gender field.
190 lines
5.5 KiB
TypeScript
190 lines
5.5 KiB
TypeScript
import type { SearchResponse, SearchResultItem, SearchSort } from "@/lib/search/types"
|
|
import type { Player, PlayerService, Shop } from "@/lib/types"
|
|
|
|
export interface SearchCatalogParams {
|
|
q?: string
|
|
selectedGames?: string[]
|
|
min?: string
|
|
max?: string
|
|
onlyOnline?: boolean
|
|
minRating?: string
|
|
sort?: SearchSort
|
|
limit?: number
|
|
offset?: number
|
|
}
|
|
|
|
export interface SearchCatalogData {
|
|
players: Player[]
|
|
shops: Shop[]
|
|
services: PlayerService[]
|
|
}
|
|
|
|
type InternalResultItem = SearchResultItem & { __index: number }
|
|
|
|
function minPriceFromServices(services: PlayerService[]) {
|
|
return services.length === 0 ? 0 : Math.min(...services.map((s) => s.price))
|
|
}
|
|
|
|
function unitFromCheapestService(services: PlayerService[]): PlayerService["unit"] {
|
|
if (services.length === 0) return "局"
|
|
return services.reduce((prev, curr) => (prev.price < curr.price ? prev : curr)).unit
|
|
}
|
|
|
|
export function searchCatalog(
|
|
params: SearchCatalogParams,
|
|
data: SearchCatalogData,
|
|
): SearchResponse {
|
|
const q = params.q ?? ""
|
|
const query = q ? q.toLowerCase() : ""
|
|
const selectedGames = params.selectedGames ?? []
|
|
|
|
const min = params.min ?? ""
|
|
const max = params.max ?? ""
|
|
const minP = min ? Number(min) : 0
|
|
const maxP = max ? Number(max) : Infinity
|
|
|
|
const onlyOnline = params.onlyOnline ?? false
|
|
const minRating = Number(params.minRating ?? "0")
|
|
const sort = params.sort ?? "composite"
|
|
|
|
const offset = Math.max(0, params.offset ?? 0)
|
|
const limit = Math.max(0, params.limit ?? 12)
|
|
|
|
const shopDerivedById = new Map<
|
|
string,
|
|
{
|
|
minPrice: number
|
|
unit: PlayerService["unit"]
|
|
games: string[]
|
|
hasAvailable: boolean
|
|
}
|
|
>()
|
|
|
|
for (const shop of data.shops) {
|
|
const shopPlayers = data.players.filter((player) => player.shopId === shop.id)
|
|
const playerIds = new Set(shopPlayers.map((player) => player.id))
|
|
const shopServices = data.services.filter((service) => playerIds.has(service.playerId))
|
|
const shopMinPrice = shopServices.length > 0 ? Math.min(...shopServices.map((s) => s.price)) : 0
|
|
const shopUnit = shopServices.length > 0 ? unitFromCheapestService(shopServices) : "局"
|
|
const shopGames = [...new Set(shopServices.map((service) => service.gameName))]
|
|
const hasAvailable = shopPlayers.some((player) => player.status === "available")
|
|
|
|
shopDerivedById.set(shop.id, {
|
|
minPrice: shopMinPrice,
|
|
unit: shopUnit,
|
|
games: shopGames,
|
|
hasAvailable,
|
|
})
|
|
}
|
|
|
|
let nextIndex = 0
|
|
const results: InternalResultItem[] = []
|
|
|
|
for (const player of data.players) {
|
|
results.push({
|
|
__index: nextIndex++,
|
|
type: "player",
|
|
player,
|
|
rating: player.rating,
|
|
orders: player.totalOrders,
|
|
minPrice: minPriceFromServices(player.services),
|
|
unit: unitFromCheapestService(player.services),
|
|
})
|
|
}
|
|
|
|
for (const shop of data.shops) {
|
|
const derived = shopDerivedById.get(shop.id)
|
|
if (!derived) continue
|
|
|
|
results.push({
|
|
__index: nextIndex++,
|
|
type: "shop",
|
|
shop,
|
|
rating: Number(shop.rating),
|
|
orders: shop.totalOrders,
|
|
minPrice: derived.minPrice,
|
|
unit: derived.unit,
|
|
games: derived.games,
|
|
hasAvailable: derived.hasAvailable,
|
|
})
|
|
}
|
|
|
|
const filtered = results.filter((item) => {
|
|
if (query) {
|
|
if (item.type === "player") {
|
|
const matchName = item.player.user.nickname.toLowerCase().includes(query)
|
|
const matchTags = item.player.tags.some((tag) => tag.toLowerCase().includes(query))
|
|
const matchGames = item.player.games.some((game) => game.toLowerCase().includes(query))
|
|
if (!matchName && !matchTags && !matchGames) return false
|
|
} else {
|
|
const matchName = item.shop.name.toLowerCase().includes(query)
|
|
const matchDescription = item.shop.description.toLowerCase().includes(query)
|
|
const matchGames = item.games.some((game) => game.toLowerCase().includes(query))
|
|
if (!matchName && !matchDescription && !matchGames) return false
|
|
}
|
|
}
|
|
|
|
if (selectedGames.length > 0) {
|
|
if (item.type === "player") {
|
|
const hasGame = item.player.games.some((game) => selectedGames.includes(game))
|
|
if (!hasGame) return false
|
|
} else {
|
|
const hasGame = item.games.some((game) => selectedGames.includes(game))
|
|
if (!hasGame) return false
|
|
}
|
|
}
|
|
|
|
if (item.minPrice < minP) return false
|
|
if (max && item.minPrice > maxP) return false
|
|
|
|
if (onlyOnline) {
|
|
if (item.type === "player") {
|
|
if (item.player.status !== "available") return false
|
|
} else {
|
|
if (!item.hasAvailable) return false
|
|
}
|
|
}
|
|
|
|
if (item.type === "player") {
|
|
if (item.player.rating < minRating) return false
|
|
} else {
|
|
if (Number(item.shop.rating) < minRating) return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
filtered.sort((a, b) => {
|
|
let diff = 0
|
|
switch (sort) {
|
|
case "rating":
|
|
diff = b.rating - a.rating
|
|
break
|
|
case "price_asc":
|
|
diff = a.minPrice - b.minPrice
|
|
break
|
|
case "price_desc":
|
|
diff = b.minPrice - a.minPrice
|
|
break
|
|
case "orders":
|
|
diff = b.orders - a.orders
|
|
break
|
|
case "composite": {
|
|
const scoreA = a.rating * Math.log10(a.orders + 1)
|
|
const scoreB = b.rating * Math.log10(b.orders + 1)
|
|
diff = scoreB - scoreA
|
|
break
|
|
}
|
|
default:
|
|
diff = 0
|
|
}
|
|
|
|
if (diff !== 0) return diff
|
|
return a.__index - b.__index
|
|
})
|
|
|
|
const total = filtered.length
|
|
const items = filtered.slice(offset, offset + limit).map(({ __index, ...item }) => item)
|
|
return { items, meta: { total, offset, limit } }
|
|
}
|