Files
IOT_SIM/server/src/index.ts
T
xuyueandClaude Opus 5.5 7cec273d58 第 2 阶段:账号与班级,作品保存在服务器
- SQLite(bun:sqlite,编号迁移):班级、用户、会话(只存令牌哈希)、作品
- 登录取代统一访问口令:老师账号由配置设定;接口和 WebSocket 都要登录;按 IP 限制输错次数
- 班级管理页:建班、改名、删除(有学生时不能删);上传 CSV 名单导入(兼容 GBK / UTF-8,
  识别表头,标出被 Excel 变成科学计数法的学号),密码留空生成 6 位数字;查看、重置密码,
  导出带密码的名单
- 学生作品自动保存到服务器(按账号和实验),显示保存状态;离开页面前立即保存
- 前端加路由:登录页、实验页、班级管理页
- 部署:TEACHER_PASSWORD 必填,数据库放在数据卷

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:36:11 +08:00

71 lines
2.1 KiB
TypeScript

import type { ClientMessage, ServerMessage } from '@iot/shared'
import { createApp } from './app'
import { ensureTeacher, sessionToken, sessionUser } from './auth'
import { compileService } from './compile-service'
import { config } from './config'
import { openDatabase } from './db'
import { cleanupStaleRuns, RunSession } from './run-session'
import { checkSandbox, sandboxError } from './sandbox'
await cleanupStaleRuns()
await checkSandbox()
if (sandboxError) console.error(sandboxError)
const db = openDatabase()
ensureTeacher(db)
const app = createApp(db)
interface SocketData {
session?: RunSession
}
const server = Bun.serve({
port: config.port,
fetch(req, server) {
if (new URL(req.url).pathname === '/ws') {
if (!sessionUser(db, sessionToken(req.headers.get('cookie')))) return new Response('请先登录', { status: 401 })
if (server.upgrade(req, { data: {} })) return
return new Response('WebSocket 升级失败', { status: 400 })
}
return app.fetch(req, server)
},
websocket: {
data: {} as SocketData,
open(ws) {
ws.data.session = new RunSession((message) => ws.send(JSON.stringify(message)))
},
async message(ws, raw) {
let message: ClientMessage
try {
message = JSON.parse(String(raw))
} catch {
return
}
const session = ws.data.session
if (!session) return
if (message.type === 'run') {
try {
await session.start(message.files)
} catch (error) {
ws.send(JSON.stringify({ type: 'serverError', message: String(error) } satisfies ServerMessage))
}
} else if (message.type === 'stop') {
await session.stop()
} else if (message.type === 'boardInput') {
session.boardInput(message)
} else if (message.type === 'boardReset') {
session.boardReset()
} else {
session.input(message)
}
},
async close(ws) {
await ws.data.session?.dispose()
},
},
})
console.log(`服务器已启动:http://localhost:${server.port}`)
await compileService.start()
console.log('C# 编译服务已就绪')