这是产品架构:
import { Schema, model, SchemaTypes } from 'mongoose';
import { IProductDocument, IReviewDocument } from '../types.js';
const ProductSchema = new Schema<IProductDocument>(
{
name: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true,
trim: true
},
price: {
type: Number,
required: true
},
imagePath: {
type: String,
required: true
},
tags: {
type: [String],
required: true,
default: []
},
sold: {
type: Number,
required: true,
default: 0
},
available: {
type: Boolean,
required: true,
default: true
},
reviews: {
type: [SchemaTypes.ObjectId],
ref: 'Review',
default: []
}
},
{
timestamps: true,
toJSON: {
virtuals: true
},
toObject: {
virtuals: true
}
}
);
ProductSchema.virtual('rating').get(async function () {
if (this.reviews.length === 0) return 0;
const reviews = this.reviews as unknown as IReviewDocument[];
const totalRating = reviews.reduce((acc, review) => {
const reviewDocument = review;
return acc + reviewDocument.rating;
}, 0);
const avgRating = totalRating / this.reviews.length;
return Math.round(avgRating * 10) / 10;
});
export default model<IProductDocument>('Product', ProductSchema);
这是路线:
router.get('/search', async (req: Request, res: Response) => {
const tagsQuery = req.query.tags ? String(req.query.tags) : '';
const priceQuery = req.query.price ? String(req.query.price) : '';
const ratingQuery = req.query.rating ? String(req.query.rating) : '';
const sortQuery = req.query.sort ? String(req.query.sort) : '';
let sort: Record<string, 1 | -1>;
switch (sortQuery) {
case 'price_asc':
sort = { price: 1 };
break;
case 'price_desc':
sort = { price: -1 };
break;
case 'recommend':
sort = { sold: -1 };
break;
case 'newest':
sort = { createdAt: -1 };
break;
case 'oldest':
sort = { createdAt: 1 };
break;
default:
sort = { sold: -1 };
break;
}
// e.g. tags=tag1,tag2 => tags = ['tag1', 'tag2'] (tags includes tag1 or tag2)
let tags = tagsQuery
.split(',')
.map((tag) => tag.trim())
.filter((tag) => Boolean(tag));
if (!tags || tags.length === 0) {
tags = ['bedroom', 'bed', 'bookshelf', 'chair', 'desk', 'drawer', 'livingroom', 'sofa', 'table'];
}
// e.g. price=0,100 => price = [0, 100] (price >= 0 && price <= 100)
const price = priceQuery
.split(',')
.map((p) => Number(p.trim()))
.filter((p) => Boolean(p));
// e.g. rating=3,5 => rating = [3, 5] (rating >= 3 && rating <= 5)
const rating = ratingQuery
.split(',')
.map((r) => Number(r.trim()))
.filter((r) => Boolean(r));
const limit = req.query.limit ? Number(req.query.limit) : 10;
const page = req.query.page ? Number(req.query.page) : 1;
const skip = (page - 1) * limit;
const pipeline = [
{
$match: {
tags: { $in: [...tags] },
price: { $gte: price[0] || -1, $lte: price[1] || Number.MAX_SAFE_INTEGER }
}
},
{
$facet: {
filteredProducts: [
{
$lookup: {
from: 'reviews',
localField: '_id',
foreignField: 'productId',
as: 'reviews'
}
},
{
$addFields: {
rating: { $round: [{ $avg: '$reviews.rating' }, 2] }
}
},
{
$unset: 'reviews'
},
{
$match: {
rating: { $gte: rating[0] || -1, $lte: rating[1] || Number.MAX_SAFE_INTEGER }
}
},
{ $sort: sort },
{ $skip: skip },
{ $limit: limit }
],
totalCount: [
{
$count: 'count'
}
]
}
}
];
const results = await Product.aggregate(pipeline);
const products = results[0].filteredProducts;
const count = results[0].totalCount[0]?.count || 0;
if (products === undefined) return res.sendStatus(404);
res.json({
products,
count,
currPage: page,
totPage: Math.ceil(count / limit)
});
});
这无法按预期工作,因为像这样查询:/search?limit=5&sort=recommend
,返回一个空的产品数组。调试了一下,我认为问题隐藏在评级过滤中,但我不确定。另外,如果可能的话,我更愿意使用猫鼬查询方法而不是聚合框架,但我不知道在如此复杂的情况下如何做到这一点。
const pipeline = [
{
$match: {
tags: { $in: tags },
price: { $gte: price[0] || 0, $lte: price[1] || Number.MAX_SAFE_INTEGER }
}
},
{
$lookup: {
from: 'reviews',
localField: '_id',
foreignField: 'productId',
as: 'reviews'
}
},
{
$addFields: {
rating: { $round: [{ $avg: '$reviews.rating' }, 2] }
}
},
{
$match: {
rating: { $gte: rating[0] || 0, $lte: rating[1] || Number.MAX_SAFE_INTEGER }
}
},
{ $sort: sort },
{ $skip: skip },
{ $limit: limit }
]