| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- #!/usr/bin/env node
- import { randomBytes } from 'node:crypto';
- const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
- const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || '';
- const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
- const FUNCTION_URL = PARSE_URL.replace(/\/parse$/, '/api/functions');
- if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY');
- const headers = {
- 'X-Parse-Application-Id': APP_ID,
- 'X-Parse-Master-Key': MASTER_KEY,
- 'Content-Type': 'application/json',
- };
- async function parse(path, init = {}) {
- const response = await fetch(`${PARSE_URL}${path}`, { ...init, headers: { ...headers, ...(init.headers || {}) } });
- const payload = await response.json().catch(() => ({}));
- if (!response.ok || payload.error) throw new Error(typeof payload.error === 'string' ? payload.error : JSON.stringify(payload.error || { status: response.status }));
- return payload;
- }
- const company = (await parse('/classes/Company?limit=1&keys=objectId')).results?.[0];
- if (!company?.objectId) throw new Error('生产 Parse 未找到 Company');
- const suffix = `${Date.now()}_${randomBytes(4).toString('hex')}`;
- const username = `schema_repair_${suffix}`;
- const password = `${randomBytes(24).toString('base64url')}Aa9!`;
- const path = `xiaoshu/system/repair-core-schema-${suffix}`;
- let userId = '';
- let functionId = '';
- const tables = ['PracticeRecord','CourseAppointment','DailyStudyRecord','CourseBinding','LessonRecord','MemoryPracticeRecord','AssessmentProfile'];
- const probeFields = {
- PracticeRecord: ['id', 'sourceKey'],
- CourseAppointment: ['id', 'sourceKey'],
- DailyStudyRecord: ['id', 'sourceKey'],
- CourseBinding: ['id', 'sourceKey'],
- LessonRecord: ['id', 'sourceKey', 'legacyGeneralId', 'lessonAt', 'sourceCreatedAt', 'studentName', 'coachName', 'courseName', 'storeId'],
- MemoryPracticeRecord: ['id', 'sourceKey'],
- AssessmentProfile: ['id', 'sourceKey'],
- };
- const code = String.raw`
- async function handler(request, response) {
- try {
- const current = request.user || (typeof user !== 'undefined' ? user : null);
- if (!current) return response.status(401).json({ success:false, message:'需要超级管理员会话' });
- await current.fetch({ useMasterKey:true });
- const roles = Array.isArray(current.get('roles')) ? current.get('roles').map(String) : [];
- if (current.get('adminRoleKey') !== 'super-admin' && !roles.includes('super-admin')) return response.status(403).json({ success:false, message:'仅超级管理员可执行' });
- const statements = [
- ${tables.map((table) => `'ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "_rperm" TEXT[]'`).join(',\n ')},
- ${tables.map((table) => `'ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "_wperm" TEXT[]'`).join(',\n ')},
- 'ALTER TABLE "PracticeRecord" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "PracticeRecord" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT',
- 'ALTER TABLE "CourseAppointment" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "CourseAppointment" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT',
- 'ALTER TABLE "DailyStudyRecord" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "DailyStudyRecord" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT',
- 'ALTER TABLE "CourseBinding" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "CourseBinding" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "legacyGeneralId" DOUBLE PRECISION',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "lessonAt" TIMESTAMPTZ',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "sourceCreatedAt" TIMESTAMPTZ',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "studentName" TEXT',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "coachName" TEXT',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "courseName" TEXT',
- 'ALTER TABLE "LessonRecord" ADD COLUMN IF NOT EXISTS "storeId" DOUBLE PRECISION',
- 'ALTER TABLE "MemoryPracticeRecord" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "MemoryPracticeRecord" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT',
- 'ALTER TABLE "AssessmentProfile" ADD COLUMN IF NOT EXISTS "id" DOUBLE PRECISION',
- 'ALTER TABLE "AssessmentProfile" ADD COLUMN IF NOT EXISTS "sourceKey" TEXT'
- ];
- for (const statement of statements) await Psql.none(statement);
- const rows = await Psql.query(
- 'SELECT table_name AS "tableName", column_name AS "columnName", data_type AS "dataType" FROM information_schema.columns WHERE table_schema=current_schema() AND table_name = ANY($1::text[]) AND column_name = ANY($2::text[]) ORDER BY table_name,column_name',
- [${JSON.stringify(tables)}, ['_rperm','_wperm','id','sourceKey','legacyGeneralId','lessonAt','sourceCreatedAt','studentName','coachName','courseName','storeId']]
- );
- response.json({ success:true, data:{ applied:statements.length, columns:rows } });
- } catch (error) {
- response.status(Number(error.status)||500).json({ success:false, message:String(error.message||error) });
- }
- }`;
- try {
- const createdUser = await parse('/users', {
- method: 'POST',
- body: JSON.stringify({
- username, password, isAdmin: true, role: 'admin', roles: ['admin','super-admin'], adminRoleKey: 'super-admin',
- company: { __type: 'Pointer', className: 'Company', objectId: company.objectId },
- realName: '核心迁移物理Schema修复临时管理员', testCreatedBy: 'repair-core-physical-schema',
- }),
- });
- userId = createdUser.objectId;
- let token = createdUser.sessionToken || '';
- if (!token) token = (await parse('/login', { method: 'POST', body: JSON.stringify({ username, password }) })).sessionToken;
- if (!token) throw new Error('临时超级管理员没有会话');
- const createdFunction = await parse('/classes/Function', {
- method: 'POST',
- body: JSON.stringify({
- name: path, desc: '一次性修复小树核心业务表物理列', type: 'standalone', path, code,
- params: [], paramList: [], respType: 'json', respJson: { success: true }, enabled: true,
- }),
- });
- functionId = createdFunction.objectId;
- const response = await fetch(`${FUNCTION_URL}/${path}`, {
- method: 'POST',
- headers: { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' },
- body: JSON.stringify({ token, params: {} }),
- });
- const payload = await response.json().catch(() => ({}));
- if (!response.ok || payload.success !== true) throw new Error(payload.message || payload.error || `物理 Schema 修复失败:${response.status}`);
- const verified = [];
- for (const [className, fields] of Object.entries(probeFields)) {
- const query = new URLSearchParams({ limit: '1', keys: ['objectId', ...fields].join(',') });
- const probe = await fetch(`${PARSE_URL}/classes/${className}?${query}`, { headers });
- const probePayload = await probe.json().catch(() => ({}));
- if (!probe.ok || probePayload.error) throw new Error(`${className} 物理列修复后仍不可读取:${JSON.stringify(probePayload)}`);
- verified.push(className);
- }
- console.log(JSON.stringify({ repaired: true, applied: payload.data?.applied || 0, columns: payload.data?.columns || [], verified }, null, 2));
- } finally {
- if (functionId) await parse(`/classes/Function/${functionId}`, { method: 'DELETE' }).catch(() => undefined);
- if (userId) {
- const where = encodeURIComponent(JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: userId } }));
- const sessions = await parse(`/classes/_Session?where=${where}&limit=1000&keys=objectId`).catch(() => ({ results: [] }));
- for (const session of sessions.results || []) await parse(`/classes/_Session/${session.objectId}`, { method: 'DELETE' }).catch(() => undefined);
- await parse(`/users/${userId}`, { method: 'DELETE' }).catch(() => undefined);
- }
- }
|