Files
2026-09-29 19:34:24 +08:00

107 lines
3.7 KiB
TypeScript

/**
* 把旧库数据编排成新库可直接写入的数据集。
*
* 逻辑集中在这里,migrate.ts(写库)与 transform.test.ts(校验)共用同一份实现,
* 避免两边各写一遍导致漂移。
*/
import { asString, type LegacyRows } from './load';
import {
HOME_COURSE_TITLE_MAP,
OPENID_RE,
transformArticle,
transformBanner,
transformCourse,
transformCourseTip,
transformEntitlement,
transformFreeLesson,
transformHomeCourse,
transformHomeFreeCourse,
transformLesson,
transformSetting,
transformUser,
} from './transform';
export type Dataset = ReturnType<typeof buildDataset>;
export function buildDataset(db: LegacyRows, staticBase: string) {
// 先建课程,得到 code → 自增 id 的映射(新库 id 从 1 开始,与插入顺序一致)
const courses = db.课程口令码读取信息.map(transformCourse);
const courseIdByCode = new Map(courses.map((c, i) => [c.code, i + 1]));
const courseIdByTitle = new Map(courses.map((c, i) => [c.title, i + 1]));
// 过滤旧库里混入的攻击载荷:只有符合 openid 形态的才当作真实用户
const skippedUsers: Array<{ openid: string; reason: string }> = [];
const users = db.用户信息.flatMap((row) => {
const openid = asString(row.OpenID);
if (!OPENID_RE.test(openid)) {
skippedUsers.push({ openid: openid.slice(0, 60), reason: '非法 openid 形态(疑似攻击载荷)' });
return [];
}
return [transformUser(row)];
});
const userIdByOpenid = new Map(users.map((u, i) => [u.openid, i + 1]));
const skippedLessonOrphans: string[] = [];
const lessons = db.视频课程数据.flatMap((row) => {
const courseId = courseIdByCode.get(asString(row.课程id));
if (!courseId) {
skippedLessonOrphans.push(asString(row.课程id));
return [];
}
return [transformLesson(row, courseId)];
});
// 权益:按 (user, course) 去重,重复的只保留第一条
const seen = new Set<string>();
const duplicateEntitlements: string[] = [];
const droppedEntitlements: string[] = [];
const entitlements = db.用户已购课程.flatMap((row) => {
const userId = userIdByOpenid.get(asString(row.OpenID));
const courseId = courseIdByCode.get(asString(row.课程id));
if (!userId || !courseId) {
droppedEntitlements.push(`${asString(row.OpenID).slice(0, 30)}::${asString(row.课程id).slice(0, 12)}`);
return [];
}
const key = `${userId}::${courseId}`;
if (seen.has(key)) {
duplicateEntitlements.push(key);
return [];
}
seen.add(key);
return [transformEntitlement(row, userId, courseId)];
});
const unmappedHomeCourses: string[] = [];
const homeCourses = db.首页课程列表.map((row) => {
const courseName = asString(row.课程名称);
const title = HOME_COURSE_TITLE_MAP[courseName];
const courseId = title ? courseIdByTitle.get(title) ?? null : null;
if (courseId === null) unmappedHomeCourses.push(courseName);
return transformHomeCourse(row, courseId, staticBase);
});
return {
courses,
users,
lessons,
entitlements,
homeCourses,
freeLessons: db.免费视频课程数据.map(transformFreeLesson),
homeFreeCourses: db.首页免费课程列表.map(transformHomeFreeCourse),
banners: db.首页横幅图片.map(transformBanner),
articles: db.网页列表.map((row) => transformArticle(row, staticBase)),
courseTips: db.课程页面Tips.map(transformCourseTip),
settings: db.cloudconfig
.map(transformSetting)
.filter((v): v is NonNullable<typeof v> => v !== null),
diagnostics: {
skippedLessonOrphans,
duplicateEntitlements,
unmappedHomeCourses,
skippedUsers,
droppedEntitlements,
},
};
}