第一次运行实验时自动建好预置的数据库

- 运行时带上当前实验;实验有 database.sql 而学生还没有其中的库时,先建好这些库
  (已有的库不动,保留学生的数据);建库失败不影响运行
- 教材项目 8 要"附加"现成的 WisdomCampus 库,学生不用先去数据库页面恢复

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 15:14:49 +08:00
1 parent 5ae07b851d
commit 10c50265b2
5 files changed
+27 -8

No files matched your search

+11 -3
View File
@@ -27,16 +27,19 @@ export async function runScript(dir: string, script: string) {
/** /**
* 把学生某个实验用到的库恢复成实验的初始数据:只替换脚本里建的库,学生的其他库不动。 * 把学生某个实验用到的库恢复成实验的初始数据:只替换脚本里建的库,学生的其他库不动。
* onlyMissing 时只建学生还没有的库(第一次运行实验时自动建库,已有的库保留学生的数据)。
* 返回恢复了哪些库。 * 返回恢复了哪些库。
*/ */
export async function restoreExperimentDatabases(userId: number, sampleId: string) { export async function restoreExperimentDatabases(userId: number, sampleId: string, onlyMissing = false) {
const script = await experimentScript(sampleId) const script = await experimentScript(sampleId)
if (!script) return [] if (!script) return []
const target = await userDatabaseDir(userId)
const existing = new Set(await readdir(target))
if (onlyMissing && scriptDatabases(script).every((name) => existing.has(`${name}.db`))) return []
const temp = await mkdtemp(join(config.runsDir, 'db-')) const temp = await mkdtemp(join(config.runsDir, 'db-'))
try { try {
await runScript(temp, script) await runScript(temp, script)
const target = await userDatabaseDir(userId) const files = (await readdir(temp)).filter((f) => f.endsWith('.db') && f !== 'master.db' && !(onlyMissing && existing.has(f)))
const files = (await readdir(temp)).filter((f) => f.endsWith('.db') && f !== 'master.db')
for (const file of files) { for (const file of files) {
for (const suffix of ['', '-wal', '-shm']) await rm(join(target, file + suffix), { force: true }) for (const suffix of ['', '-wal', '-shm']) await rm(join(target, file + suffix), { force: true })
await cp(join(temp, file), join(target, file)) await cp(join(temp, file), join(target, file))
@@ -47,6 +50,11 @@ export async function restoreExperimentDatabases(userId: number, sampleId: strin
} }
} }
/** 脚本里 CREATE DATABASE 建的库名 */
function scriptDatabases(script: string) {
return [...script.matchAll(/^\s*CREATE\s+DATABASE\s+\[?([^\s\]]+)\]?/gim)].map((m) => m[1])
}
/** 评分用:实验数据库的模板按实验缓存,每个检查项复制一份(互不影响) */ /** 评分用:实验数据库的模板按实验缓存,每个检查项复制一份(互不影响) */
const templates = new Map<string, Promise<string | undefined>>() const templates = new Map<string, Promise<string | undefined>>()
+7 -1
View File
@@ -7,7 +7,8 @@ import { languageService } from './language-service'
import { config } from './config' import { config } from './config'
import { openDatabase } from './db' import { openDatabase } from './db'
import { createGradingQueue } from './grading/queue' import { createGradingQueue } from './grading/queue'
import { userDatabaseDir } from './databases' import { restoreExperimentDatabases, userDatabaseDir } from './databases'
import { listSamples } from './samples'
import { cleanupStaleRuns, RunSession } from './run-session' import { cleanupStaleRuns, RunSession } from './run-session'
import { sqlService } from './sql-service' import { sqlService } from './sql-service'
import { checkSandbox, sandboxError } from './sandbox' import { checkSandbox, sandboxError } from './sandbox'
@@ -56,6 +57,11 @@ const server = Bun.serve({
if (!session) return if (!session) return
if (message.type === 'run') { if (message.type === 'run') {
try { try {
const { sampleId } = message
if (typeof sampleId === 'string' && (await listSamples()).some((s) => s.id === sampleId)) {
// 建库失败不影响运行(程序连不上库时会报出来)
await restoreExperimentDatabases(ws.data.userId!, sampleId, true).catch((error) => console.error('自动建实验数据库失败', error))
}
await session.start(message.files) await session.start(message.files)
} catch (error) { } catch (error) {
ws.send(JSON.stringify({ type: 'serverError', message: String(error) } satisfies ServerMessage)) ws.send(JSON.stringify({ type: 'serverError', message: String(error) } satisfies ServerMessage))
+2 -1
View File
@@ -155,7 +155,8 @@ export type RunnerInput =
/** 浏览器发给服务器的消息 */ /** 浏览器发给服务器的消息 */
export type ClientMessage = export type ClientMessage =
| { type: 'run'; files: SourceFile[] } /** sampleId:当前实验,第一次运行时自动建好实验预置的数据库(学生还没有这些库时) */
| { type: 'run'; files: SourceFile[]; sampleId?: string }
/** 调节实训台环境:拖动滑块、触发传感器 */ /** 调节实训台环境:拖动滑块、触发传感器 */
/** clock:拨到某个时刻(time:yyyy-MM-ddTHH:mm:ss)、改倍速,或 null 恢复真实时间 */ /** clock:拨到某个时刻(time:yyyy-MM-ddTHH:mm:ss)、改倍速,或 null 恢复真实时间 */
| { type: 'boardInput'; analog?: Partial<BoardState['analog']>; digital?: Partial<BoardState['digital']>; clock?: { time?: string; speed?: number } | null } | { type: 'boardInput'; analog?: Partial<BoardState['analog']>; digital?: Partial<BoardState['digital']>; clock?: { time?: string; speed?: number } | null }
+4 -1
View File
@@ -409,7 +409,10 @@ function restoreOriginal() {
function run() { function run() {
if (running.value) return if (running.value) return
session.run(files.value.map((f) => ({ ...f }))) session.run(
files.value.map((f) => ({ ...f })),
props.assignmentId ? assignment.value?.sampleId : sampleId.value,
)
} }
</script> </script>
+3 -2
View File
@@ -109,7 +109,8 @@ export function createRunSession() {
socket?.send(JSON.stringify(message)) socket?.send(JSON.stringify(message))
} }
async function run(files: SourceFile[]) { /** sampleId:当前实验(服务器据此在第一次运行时建好实验预置的数据库) */
async function run(files: SourceFile[], sampleId?: string) {
await connect() await connect()
state.diagnostics = [] state.diagnostics = []
state.console = '' state.console = ''
@@ -121,7 +122,7 @@ export function createRunSession() {
nodes.clear() nodes.clear()
for (const url of images.values()) URL.revokeObjectURL(url) for (const url of images.values()) URL.revokeObjectURL(url)
images.clear() images.clear()
send({ type: 'run', files }) send({ type: 'run', files, sampleId })
} }
/** 调节实训台环境(滑块、传感器开关) */ /** 调节实训台环境(滑块、传感器开关) */