Skip to content

Commit 6a50fc1

Browse files
committed
fix: punctuation-proof review grouping and catalog-aware course attachment
- group keys strip whitespace and unify 、/,/·/- variants so courses split by a single punctuation mark merge into one review group - review groups resolve course attachment through the handbook catalog (name + aliases) before falling back to unmatched, so catalog-named and legacy-variant reviews attach to manifest course pages (mini-program course detail reviews included)
1 parent 3e4a8de commit 6a50fc1

3 files changed

Lines changed: 51 additions & 4 deletions

File tree

‎server/public-api-dto.mjs‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,19 @@ export function normalizeReviewKeyPart(value) {
3131
return text(value, 120);
3232
}
3333

34+
// 分组键专用:抹平空格与顿号/逗号/中点等符号差异,
35+
// 避免同一门课因「、/,」「多一个空格」被拆成两个评价组。
36+
function groupKeyPart(value) {
37+
return normalizeReviewKeyPart(value)
38+
.replace(/\s+/g, "")
39+
.replace(/[、,,.。::;;·—\-_/\\]/g, "")
40+
.replace(/(/g, "(")
41+
.replace(/)/g, ")");
42+
}
43+
3444
export function reviewGroupKey(courseTitle, teacher) {
3545
return createHash("sha256")
36-
.update(`${normalizeReviewKeyPart(courseTitle)}\0${normalizeReviewKeyPart(teacher)}`, "utf8")
46+
.update(`${groupKeyPart(courseTitle)}\0${groupKeyPart(teacher)}`, "utf8")
3747
.digest("base64url")
3848
.slice(0, 24);
3949
}
@@ -100,20 +110,32 @@ export function publicReviewDto(review) {
100110
};
101111
}
102112

103-
export function buildReviewGroups(manifest, reviewData) {
113+
export function buildReviewGroups(manifest, reviewData, courseCatalog = null) {
104114
const coursesByTitle = new Map();
105115
for (const course of manifest.courses || []) {
106116
const title = normalizeReviewKeyPart(course.title);
107117
if (!coursesByTitle.has(title)) coursesByTitle.set(title, course);
108118
}
119+
const resolveCourse = (courseTitle) => {
120+
const direct = coursesByTitle.get(courseTitle);
121+
if (direct) return direct;
122+
// 经课程目录解析:评价课程名 -> 目录条目(含别名) -> 再尝试其规范名与别名匹配课程库
123+
const entry = courseCatalog?.find?.(courseTitle);
124+
if (!entry) return null;
125+
for (const candidate of [entry.name, ...(entry.aliases || [])]) {
126+
const hit = coursesByTitle.get(normalizeReviewKeyPart(candidate));
127+
if (hit) return hit;
128+
}
129+
return null;
130+
};
109131
const groups = new Map();
110132
for (const review of reviewData?.reviews || []) {
111133
if (!approvedReview(review)) continue;
112134
const courseTitle = normalizeReviewKeyPart(review.courseTitle);
113135
const teacher = normalizeReviewKeyPart(review.teacher);
114136
if (!courseTitle || !teacher) continue;
115137
const key = reviewGroupKey(courseTitle, teacher);
116-
const course = coursesByTitle.get(courseTitle) || null;
138+
const course = resolveCourse(courseTitle);
117139
if (!groups.has(key)) {
118140
groups.set(key, { key, courseTitle, teacher, course, reviews: [] });
119141
}

‎server/public-api-service.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class PublicApiService {
7575
const manifest = this.readManifest();
7676
const reviewData = this.readReviews();
7777
if (!manifest || !Array.isArray(manifest.courses)) throw new Error("Runtime course data is unavailable.");
78-
const groups = buildReviewGroups(manifest, reviewData);
78+
const groups = buildReviewGroups(manifest, reviewData, this.courseCatalog);
7979
const normalizedGuides = normalizeGuideData(this.readGuides(), {
8080
courseIds: new Set(manifest.courses.map((course) => course.uid)),
8181
fallbackCorrectionUrl: this.guideCorrectionUrl,

‎test/public-api.test.mjs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,28 @@ test("mini-program review body maps into the shared submission service", async (
200200
assert.equal(submissions[1].input.content, "足够长的课程评价正文内容。");
201201
assert.equal(Object.hasOwn(submissions[1].input, "anonymous"), false);
202202
});
203+
204+
test("review groups merge punctuation variants and resolve catalog aliases", async () => {
205+
const { buildReviewGroups } = await import("../server/public-api-dto.mjs");
206+
const manifest = { courses: [{ id: "c1", uid: "u1", title: "光、视觉与艺术" }] };
207+
const reviewData = {
208+
reviews: [
209+
{ id: "r1", courseTitle: "光、视觉与艺术", teacher: "张三", rating: 5, content: "a", status: "approved", createdAt: "2026-01-01" },
210+
{ id: "r2", courseTitle: "光,视觉与艺术", teacher: "张三", rating: 4, content: "b", status: "approved", createdAt: "2026-01-02" },
211+
{ id: "r3", courseTitle: "人工智能与创新 (C++)", teacher: "李四", rating: 5, content: "c", status: "approved", createdAt: "2026-01-03" },
212+
{ id: "r4", courseTitle: "人工智能与创新(C++)", teacher: "李四", rating: 3, content: "d", status: "approved", createdAt: "2026-01-04" },
213+
],
214+
};
215+
const groups = buildReviewGroups(manifest, reviewData);
216+
assert.equal(groups.length, 2, "punctuation variants merge into one group each");
217+
const guang = groups.find((g) => g.courseTitle === "光、视觉与艺术");
218+
assert.equal(guang.reviews.length, 2);
219+
assert.equal(guang.course?.id, "c1", "direct title attaches the course");
220+
221+
// 目录别名解析挂接
222+
const fakeCatalog = { find: (name) => (name.includes("视觉") ? { name: "光、视觉与艺术", aliases: [] } : null) };
223+
const manifest2 = { courses: [{ id: "c2", uid: "u2", title: "光、视觉与艺术" }] };
224+
const reviewData2 = { reviews: [{ id: "r5", courseTitle: "光,视觉与艺术", teacher: "王五", rating: 5, content: "e", status: "approved", createdAt: "2026-01-05" }] };
225+
const groups2 = buildReviewGroups(manifest2, reviewData2, fakeCatalog);
226+
assert.equal(groups2[0].course?.id, "c2", "catalog alias resolution attaches unmatched titles");
227+
});

0 commit comments

Comments
 (0)