feat: AI Directory Platform 底座升级 + 五套主题站点
- 数据模型: vote/review/submission/claim/use_case/audience/collection/event (+tool 冗余统计列) - 社区: 投票(限流+原子toggle+API) / 评论(评分+优缺点+Helpful+聚合回写) / 提交(重复检测+审批发布) / 认领(Owner白名单编辑) - Ranking Engine: trending/popular/most-voted/most-saved/highest-rated/new/editors-choice (纯函数可测) - 发现体系: Use Case + Audience + Collection 三维发现 / /ai-tools-for / /best SEO 页 - Programmatic SEO: 页面生成器 / sitemap 270条 / AggregateRating/FAQPage/Breadcrumb/ItemList schema / alternatives + vs 对比页 - Analytics: event 事件流 + 外链跳转埋点 + /admin/analytics CTR 与搜索词分析 - Admin: submissions/claims/reviews/discovery/vote-audit/analytics 六个新后台 + 仪表盘扩展 - 主题系统: themes registry + 五套站点 (classic/product/editorial/niche/minimal) 构建期注入 - 认证: GitHub OAuth (env门控) + 登录回跳 redirect - i18n: 新增 7 个命名空间中英文案 (546 键一致) - 测试: bun test 32 用例 (ranking/slug/去重/SEO生成器) + Playwright e2e 脚本 - 种子: 14分类/16用例/9受众/40工具/6用户/投票/评论/6合集/7718事件
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
trendingScore,
|
||||
bayesianRating,
|
||||
timeDecayFactor,
|
||||
popularScore,
|
||||
parseToolSlugRoute,
|
||||
normalizeDomain,
|
||||
slugify,
|
||||
type ToolSignals,
|
||||
} from "../src/lib/ranking/score";
|
||||
|
||||
// ========== Trending 评分 ==========
|
||||
|
||||
describe("trendingScore", () => {
|
||||
const baseSignals: ToolSignals = {
|
||||
votesInWindow: 10,
|
||||
favoritesInWindow: 5,
|
||||
reviewsInWindow: 2,
|
||||
viewsInWindow: 100,
|
||||
ageHours: 24,
|
||||
};
|
||||
|
||||
test("互动越多分数越高", () => {
|
||||
const more: ToolSignals = { ...baseSignals, votesInWindow: 50 };
|
||||
expect(trendingScore(more)).toBeGreaterThan(trendingScore(baseSignals));
|
||||
});
|
||||
|
||||
test("无互动分数为 0(避免空榜填充)", () => {
|
||||
const empty: ToolSignals = {
|
||||
votesInWindow: 0,
|
||||
favoritesInWindow: 0,
|
||||
reviewsInWindow: 0,
|
||||
viewsInWindow: 0,
|
||||
ageHours: 100,
|
||||
};
|
||||
expect(trendingScore(empty)).toBe(0);
|
||||
});
|
||||
|
||||
test("新工具衰减慢:相同互动下,新工具 > 老工具(防止老工具永久霸榜)", () => {
|
||||
const fresh = trendingScore({ ...baseSignals, ageHours: 24 });
|
||||
const old = trendingScore({ ...baseSignals, ageHours: 24 * 365 });
|
||||
expect(fresh).toBeGreaterThan(old);
|
||||
});
|
||||
|
||||
test("评论权重高于投票", () => {
|
||||
const reviewHeavy = trendingScore({ ...baseSignals, reviewsInWindow: 3, votesInWindow: 0 });
|
||||
const voteHeavy = trendingScore({ ...baseSignals, reviewsInWindow: 0, votesInWindow: 3 });
|
||||
expect(reviewHeavy).toBeGreaterThan(voteHeavy);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 贝叶斯评分 ==========
|
||||
|
||||
describe("bayesianRating", () => {
|
||||
test("评论极少时被先验拉低", () => {
|
||||
const oneFiveStar = bayesianRating(5, 1, 4.0);
|
||||
expect(oneFiveStar).toBeLessThan(5);
|
||||
});
|
||||
|
||||
test("评论很多时接近观测均值", () => {
|
||||
const manyReviews = bayesianRating(4.8, 200, 4.0);
|
||||
expect(manyReviews).toBeGreaterThan(4.7);
|
||||
expect(manyReviews).toBeLessThanOrEqual(4.8);
|
||||
});
|
||||
|
||||
test("样本相同的两工具,均值高者胜", () => {
|
||||
expect(bayesianRating(4.8, 10, 4.0)).toBeGreaterThan(bayesianRating(4.2, 10, 4.0));
|
||||
});
|
||||
|
||||
test("无评论返回 0", () => {
|
||||
expect(bayesianRating(5, 0, 4)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 时间衰减 / Popular ==========
|
||||
|
||||
describe("timeDecayFactor", () => {
|
||||
test("未衰减为 1,随时间单调递减", () => {
|
||||
expect(timeDecayFactor(0)).toBe(1);
|
||||
expect(timeDecayFactor(100)).toBeLessThan(1);
|
||||
expect(timeDecayFactor(200)).toBeLessThan(timeDecayFactor(100));
|
||||
});
|
||||
|
||||
test("半衰期处恰好为 0.5", () => {
|
||||
expect(timeDecayFactor(168, 168)).toBeCloseTo(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("popularScore", () => {
|
||||
test("全量互动加权求和", () => {
|
||||
expect(popularScore(10, 10, 10, 1000)).toBe(10 * 2 + 10 * 1.5 + 10 * 3 + 1000 * 0.1);
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 复合路由解析(Alternatives / Comparison SEO) ==========
|
||||
|
||||
describe("parseToolSlugRoute", () => {
|
||||
test("普通工具", () => {
|
||||
expect(parseToolSlugRoute("chatgpt")).toEqual({ kind: "tool", slug: "chatgpt" });
|
||||
});
|
||||
|
||||
test("alternatives 页", () => {
|
||||
expect(parseToolSlugRoute("chatgpt-alternatives")).toEqual({
|
||||
kind: "alternatives",
|
||||
base: "chatgpt",
|
||||
});
|
||||
});
|
||||
|
||||
test("vs 对比页", () => {
|
||||
expect(parseToolSlugRoute("cursor-vs-windsurf")).toEqual({
|
||||
kind: "comparison",
|
||||
a: "cursor",
|
||||
b: "windsurf",
|
||||
});
|
||||
});
|
||||
|
||||
test("名称中带连字符的对比页", () => {
|
||||
expect(parseToolSlugRoute("github-copilot-vs-tabnine")).toEqual({
|
||||
kind: "comparison",
|
||||
a: "github-copilot",
|
||||
b: "tabnine",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========== 域名归一化(重复工具检测) ==========
|
||||
|
||||
describe("normalizeDomain", () => {
|
||||
test("https URL 提取主机", () => {
|
||||
expect(normalizeDomain("https://chatgpt.com/")).toBe("chatgpt.com");
|
||||
});
|
||||
|
||||
test("去 www", () => {
|
||||
expect(normalizeDomain("www.anthropic.com")).toBe("anthropic.com");
|
||||
});
|
||||
|
||||
test("带路径与查询参数", () => {
|
||||
expect(normalizeDomain("https://openai.com/chatgpt?ref=x")).toBe("openai.com");
|
||||
});
|
||||
|
||||
test("子域保留(重复检测按主机比较)", () => {
|
||||
expect(normalizeDomain("http://chat.openai.com")).toBe("chat.openai.com");
|
||||
});
|
||||
|
||||
test("非法输入返回 null", () => {
|
||||
expect(normalizeDomain("")).toBeNull();
|
||||
expect(normalizeDomain("not a url")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ========== slug 生成 ==========
|
||||
|
||||
describe("slugify", () => {
|
||||
test("空格转连字符 + 小写", () => {
|
||||
expect(slugify("Best AI Writing Tools")).toBe("best-ai-writing-tools");
|
||||
});
|
||||
|
||||
test("去除特殊字符", () => {
|
||||
expect(slugify("DALL·E 3!")).toBe("dalle-3");
|
||||
});
|
||||
|
||||
test("合并连续连字符", () => {
|
||||
expect(slugify("a -- b")).toBe("a-b");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user