四端上传
This commit is contained in:
1 parent
25bbb5f9c8
commit
c6e2cb4d5b
276 files changed
+17812
-3
No files matched your search
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 旧行 → 新行 的纯转换函数。
|
||||
*
|
||||
* 本文件不碰数据库、不做 IO,所有输入输出都是普通对象,便于用测试覆盖。
|
||||
* 数据库读写与顺序编排在 migrate.ts。
|
||||
*/
|
||||
|
||||
import type { SqlValue } from './parser';
|
||||
|
||||
export const SOURCE_ORDER = 1;
|
||||
export const SOURCE_LEGACY = 2;
|
||||
export const SOURCE_MANUAL = 3;
|
||||
|
||||
/** 旧库时间的时区。旧数据由服务器本地 NOW() 写入,服务器在中国大陆。 */
|
||||
const LEGACY_UTC_OFFSET = process.env.LEGACY_UTC_OFFSET ?? '+08:00';
|
||||
|
||||
function str(v: SqlValue | undefined): string {
|
||||
return v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
|
||||
function strOrNull(v: SqlValue | undefined): string | null {
|
||||
const s = str(v).trim();
|
||||
return s === '' ? null : s;
|
||||
}
|
||||
|
||||
function int(v: SqlValue | undefined): number {
|
||||
const n = Number(str(v));
|
||||
return Number.isFinite(n) ? Math.trunc(n) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析旧库时间字符串。
|
||||
*
|
||||
* 旧值有两种形态,都是服务器本地墙钟时间:
|
||||
* `2024-04-11 00:00:39` 日期 + 时间
|
||||
* `2024-02-01` 仅日期(旧库中确实存在,之前未处理会解析失败)
|
||||
*
|
||||
* 需按 +08:00 换算为 UTC 存储。若值本身已带时区信息则直接采用。
|
||||
*/
|
||||
export function parseLegacyDate(v: SqlValue | undefined): Date | null {
|
||||
const raw = str(v).trim();
|
||||
if (raw === '' || raw.startsWith('0000-00-00')) return null;
|
||||
|
||||
let normalized = raw.replace(' ', 'T');
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) normalized += 'T00:00:00';
|
||||
|
||||
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/.test(normalized);
|
||||
const date = new Date(hasZone ? normalized : `${normalized}${LEGACY_UTC_OFFSET}`);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
throw new Error(`无法解析时间:${JSON.stringify(raw)}`);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/** 把内容字段里的旧域名替换为新静态资源域名。 */
|
||||
export function rewriteDomain(html: string | null, newBase: string): string | null {
|
||||
if (html === null) return null;
|
||||
return html.replace(/https?:\/\/api\.yeomai\.site/gi, newBase.replace(/\/+$/, ''));
|
||||
}
|
||||
|
||||
export type Row = Record<string, SqlValue>;
|
||||
|
||||
// ── users ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function transformUser(row: Row) {
|
||||
const registeredAt = parseLegacyDate(row.注册日期);
|
||||
return {
|
||||
openid: str(row.OpenID),
|
||||
nickname: strOrNull(row.用户名)?.slice(0, 32) ?? null,
|
||||
headicon_id: int(row.HeadiconID),
|
||||
status: 1,
|
||||
registered_at: registeredAt ?? new Date(0),
|
||||
last_login_at: parseLegacyDate(row.最后登录日期),
|
||||
};
|
||||
}
|
||||
|
||||
// ── courses ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function transformCourse(row: Row) {
|
||||
return {
|
||||
code: str(row.课程id),
|
||||
// 旧数据存在 '7年级同步课程\r\n' 这类首尾换行,必须清理
|
||||
title: str(row.课程名称).trim().slice(0, 128),
|
||||
intro: strOrNull(row.课程介绍)?.slice(0, 255) ?? null,
|
||||
cover_image: null,
|
||||
price_fen: 0,
|
||||
status: 1,
|
||||
sort_order: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ── lessons ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function transformLesson(row: Row, courseId: number) {
|
||||
return {
|
||||
course_id: courseId,
|
||||
play_id: str(row.播放id),
|
||||
chapter_id: str(row.章节id),
|
||||
chapter_title: strOrNull(row.章节名称),
|
||||
lesson_title: strOrNull(row.视频名称),
|
||||
duration: strOrNull(row.时长),
|
||||
published_at: parseLegacyDate(row.发布日期),
|
||||
cover_image: strOrNull(row.封面图片),
|
||||
sort_order: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ── entitlements ─────────────────────────────────────────────────────────
|
||||
|
||||
export function transformEntitlement(row: Row, userId: number, courseId: number) {
|
||||
const grantedAt = parseLegacyDate(row.购课日期);
|
||||
return {
|
||||
user_id: userId,
|
||||
course_id: courseId,
|
||||
source: SOURCE_LEGACY,
|
||||
ref_id: null,
|
||||
granted_at: grantedAt ?? new Date(0),
|
||||
expire_at: null,
|
||||
note: '旧版课程码兑换/历史购买记录',
|
||||
};
|
||||
}
|
||||
|
||||
// ── free_lessons ─────────────────────────────────────────────────────────
|
||||
|
||||
export function transformFreeLesson(row: Row) {
|
||||
return {
|
||||
series_no: int(row.序号),
|
||||
sort_order: int(row.排序),
|
||||
course_name: str(row.课程名称),
|
||||
video_title: strOrNull(row.视频标题),
|
||||
intro: strOrNull(row.详细介绍),
|
||||
publisher: strOrNull(row.发布人),
|
||||
published_at: parseLegacyDate(row.发布日期),
|
||||
duration: strOrNull(row.时长),
|
||||
video_m3u8: strOrNull(row.视频m3u8),
|
||||
cover_image: strOrNull(row.封面图片),
|
||||
};
|
||||
}
|
||||
|
||||
// ── home_courses ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 首页长图条目 → 可购买课程 的对应关系。
|
||||
*
|
||||
* 首页 5 条「新课上线」与 10 门付费课程的命名并不一致(七/7、封面/封面2 等后缀),
|
||||
* 无法用规则推导,这里显式列出,且用课程标题做键以便阅读。
|
||||
*/
|
||||
export const HOME_COURSE_TITLE_MAP: Record<string, string> = {
|
||||
几何模型封面: '几何模型',
|
||||
中考数学压轴大招封面: '中考数学压轴大招',
|
||||
七年级同步课程封面2: '7年级同步课程',
|
||||
八年级同步课程: '8年级同步课程',
|
||||
九年级同步课程: '9年级同步课程',
|
||||
};
|
||||
|
||||
export function transformHomeCourse(row: Row, courseId: number | null, staticBase: string) {
|
||||
return {
|
||||
course_name: str(row.课程名称),
|
||||
intro: strOrNull(row.课程介绍),
|
||||
published_at: parseLegacyDate(row.课程日期),
|
||||
cover_image: strOrNull(row.课程封面图片),
|
||||
content: rewriteDomain(strOrNull(row.内容), staticBase),
|
||||
course_id: courseId,
|
||||
sort_order: 0,
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// ── home_free_courses ────────────────────────────────────────────────────
|
||||
|
||||
export function transformHomeFreeCourse(row: Row) {
|
||||
return {
|
||||
series_no: int(row.序号),
|
||||
course_name: str(row.课程名称),
|
||||
intro: strOrNull(row.课程介绍),
|
||||
published_at: parseLegacyDate(row.课程日期),
|
||||
cover_image: strOrNull(row.课程封面图片),
|
||||
sort_order: int(row.序号),
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// ── banners ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function transformBanner(row: Row) {
|
||||
return {
|
||||
filename: str(row.图片名),
|
||||
sort_order: int(row.序号),
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// ── articles ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function transformArticle(row: Row, staticBase: string) {
|
||||
return {
|
||||
legacy_page_id: strOrNull(row.网页id),
|
||||
title: str(row.网页标题),
|
||||
intro: strOrNull(row.详细介绍),
|
||||
publisher: strOrNull(row.发布人),
|
||||
published_at: parseLegacyDate(row.发布日期),
|
||||
content: rewriteDomain(strOrNull(row.网页内容), staticBase),
|
||||
sort_order: int(row.网页id),
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// ── course_tips ──────────────────────────────────────────────────────────
|
||||
|
||||
export function transformCourseTip(row: Row) {
|
||||
return {
|
||||
tip_key: int(row.id),
|
||||
content: str(row.content),
|
||||
title: strOrNull(row.title),
|
||||
};
|
||||
}
|
||||
|
||||
// ── settings ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** cloudconfig 数字 ID → settings 字符串 key。ID 0「占空」为无意义占位,不迁移。 */
|
||||
export const SETTINGS_KEY_MAP: Record<number, string> = {
|
||||
1: 'site.version',
|
||||
2: 'home.title.paid',
|
||||
3: 'home.title.trial',
|
||||
4: 'home.title.public',
|
||||
5: 'profile.qrcode_image',
|
||||
6: 'profile.headicon_count',
|
||||
7: 'profile.menu.change_info',
|
||||
8: 'profile.menu.share',
|
||||
9: 'profile.menu.contact',
|
||||
10: 'profile.menu.wechat',
|
||||
11: 'home.footer_text',
|
||||
12: 'profile.qrcode_caption',
|
||||
};
|
||||
|
||||
export function transformSetting(row: Row): { key: string; value: string | null; remark: string | null } | null {
|
||||
const key = SETTINGS_KEY_MAP[int(row.ID)];
|
||||
if (!key) return null;
|
||||
return { key, value: strOrNull(row.value), remark: strOrNull(row.注释) };
|
||||
}
|
||||
Reference in new issue
Block a user