项目 6 数据存储:仿冒 SqlClient(T-SQL 转写成 SQLite)、数据库页面、DataGridView、DateTimePicker
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- 每个学生一套数据库,长期保存(data/databases/<用户编号>/库名.db);学生程序运行时挂进沙箱可读写
- Sim.Data:SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter;
  连接字符串只按库名打开学生自己的库;ScriptDom 解析 T-SQL 后按白名单转写成 SQLite 语句,
  STRICT 表 + 元数据表还原 SQL Server 的列类型,报错编号和中文信息与 SQL Server 一致;
  INSERT 不写列名时跳过自增列,日期统一格式(书上 DateTime.ToString() 拼接的日期能正确比较)
- 常驻 SQL 服务(Sim.Runner sql-server):数据库页面的查询、对象资源管理器,评分前按实验 database.sql 建库
- 数据库页面(代替 SSMS):对象资源管理器、查询窗口、编辑前 200 行、表设计器、恢复实验初始数据
- DataGridView(绑定 DataView/DataTable/DataSet,单元格修改写回 DataTable)、DateTimePicker;设计器工具箱加入
- 评分:每个检查项一份新建的实验库;新增点 / 修改表格单元格、选日期、表格里有 / 没有某行的检查
- 示例:项目 6 任务 2、3;时间格式改成和中文 Windows 一致(9:05:03);异常堆栈不暴露服务器路径
- 测试:数据库端到端、接口(每人只看到自己的库、恢复实验数据)、评分出错情况、T-SQL 生成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 09:18:56 +08:00
1 parent 6b088240da
commit 670abe5921
72 files changed
+7253 -51

No files matched your search

+51
View File
@@ -1,5 +1,6 @@
// HTTP 接口测试:内存数据库,直接调用 app.fetch。
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
import { rm } from 'node:fs/promises'
import { createAiQueue, normalizeAiResult } from '../src/ai-review'
import { createApp } from '../src/app'
import { ensureTeacher } from '../src/auth'
@@ -7,6 +8,7 @@ import { config } from '../src/config'
import { openDatabase } from '../src/db'
import { createGradingQueue, type GradingQueue } from '../src/grading/queue'
import { languageService } from '../src/language-service'
import { sqlService } from '../src/sql-service'
import { loadSample } from '../src/samples'
let app: ReturnType<typeof createApp>
@@ -434,6 +436,55 @@ describe('语言服务', () => {
}, 60000)
})
describe('数据库页面', () => {
const saved = config.dataDir
// 测试的用户编号会和本机开发环境重复,数据库放到单独的目录
beforeAll(async () => {
config.dataDir = `${config.runsDir}/test-api-data`
await rm(config.dataDir, { recursive: true, force: true })
})
afterAll(() => {
config.dataDir = saved
sqlService.stop()
})
test('建库建表、查询;SQL 报错和 SSMS 一样返回编号和信息;每个人只看到自己的库', async () => {
const t = await teacher()
await classWithStudents(t, [{ username: 's1', name: '甲', password: 'p1' }])
const student = new Browser()
await student.login('s1', 'p1')
expect((await t.post('/api/db/query', { database: 'master', sql: 'CREATE DATABASE 测试库' })).body).toMatchObject({ type: 'result', affected: -1 })
const created = await t.post('/api/db/query', {
database: '测试库',
sql: "CREATE TABLE T (ID int IDENTITY(1,1) PRIMARY KEY, name nvarchar(20)); INSERT INTO T (name) VALUES (N'张三'), (N'李四'); SELECT * FROM T WHERE name LIKE N'张%'",
})
expect(created.body).toMatchObject({ type: 'result', database: '测试库', affected: 2, results: [{ columns: [{ name: 'ID', type: 'int' }, { name: 'name', type: 'nvarchar(20)' }], rows: [[1, '张三']], total: 1 }] })
const error = await t.post('/api/db/query', { database: '测试库', sql: 'SELECT * FROM 不存在的表' })
expect(error.body).toEqual({ type: 'error', number: 208, message: "对象名 '不存在的表' 无效。", line: 1 })
const explorer = (await t.get('/api/db')).body
expect(explorer.databases).toEqual([{ name: '测试库', tables: [{ name: 'T', columns: [
{ name: 'ID', type: 'int', nullable: false, identity: true, primaryKey: true },
{ name: 'name', type: 'nvarchar(20)', nullable: true, identity: false, primaryKey: false },
] }] }])
expect((await student.get('/api/db')).body.databases).toEqual([])
expect((await new Browser().get('/api/db')).status).toBe(401)
}, 60000)
test('恢复实验初始数据:只替换实验用到的库', async () => {
const t = await teacher()
await t.post('/api/db/query', { database: 'master', sql: 'CREATE DATABASE 我的库' })
expect((await t.get('/api/db/experiments')).body.map((e: { id: string }) => e.id)).toContain('p6-t2-存储环境数据')
expect((await t.post('/api/db/restore', { sampleId: 'p6-t2-存储环境数据' })).body).toEqual({ databases: ['环境数据表'] })
const rows = await t.post('/api/db/query', { database: '环境数据表', sql: 'SELECT COUNT(*) FROM CampusEnvironment' })
expect(rows.body.results[0].rows).toEqual([[3]])
expect((await t.get('/api/db')).body.databases.map((d: { name: string }) => d.name)).toEqual(['我的库', '测试库', '环境数据表'])
expect((await t.post('/api/db/restore', { sampleId: '../etc' })).status).toBe(400)
}, 60000)
})
describe('AI 评阅', () => {
const SAMPLE = 'p4-t3-报警灯'
const scoring = { auto: [10, 10], manual: [{ title: '窗体界面', points: 10 }, { title: '注释规范', points: 5 }] }