项目 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

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,143 @@
using System.Drawing;
using System.Globalization;
using System.Text.Json.Nodes;
using Sim.Host;
namespace System.Windows.Forms;
public enum DateTimePickerFormat { Long = 1, Short = 2, Time = 4, Custom = 8 }
/// <summary>日期时间选择器(教材项目 6 任务 3 选查询的起止日期)。Text 按 Format 显示,和中文 Windows 一致(Long:2024年1月16日)</summary>
public class DateTimePicker : Control
{
public static readonly DateTime MinimumDateTime = new(1753, 1, 1);
public static readonly DateTime MaximumDateTime = new(9998, 12, 31);
private DateTime _value = DateTime.Now;
private DateTimePickerFormat _format = DateTimePickerFormat.Long;
private string? _customFormat;
private bool _showUpDown;
private DateTime _minDate = MinimumDateTime;
private DateTime _maxDate = MaximumDateTime;
protected override Size DefaultSize => new(200, 21);
public DateTime Value
{
get => _value;
set
{
if (value < _minDate || value > _maxDate)
throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“Value”无效。“Value”应介于“MinDate”和“MaxDate”之间。");
if (value == _value) return;
LegacyThreads.CheckPoint();
_value = value;
NotifyValue();
OnValueChanged(EventArgs.Empty);
}
}
public DateTimePickerFormat Format
{
get => _format;
set
{
_format = value;
NotifyValue();
NotifyChanged(nameof(Format), value);
}
}
public string? CustomFormat
{
get => _customFormat;
set
{
_customFormat = value;
NotifyValue();
}
}
public bool ShowUpDown
{
get => _showUpDown;
set => SetProp(ref _showUpDown, value, nameof(ShowUpDown));
}
public DateTime MinDate
{
get => _minDate;
set => _minDate = value;
}
public DateTime MaxDate
{
get => _maxDate;
set => _maxDate = value;
}
public bool ShowCheckBox { get; set; }
public bool Checked { get; set; } = true;
public LeftRightAlignment DropDownAlign { get; set; }
/// <summary>显示的文字;赋值时按日期解析</summary>
public override string Text
{
get => Display;
set
{
if (string.IsNullOrEmpty(value)) return;
Value = DateTime.Parse(value, CultureInfo.CurrentCulture);
}
}
private string Display => _format switch
{
DateTimePickerFormat.Short => _value.ToString("d", CultureInfo.CurrentCulture),
DateTimePickerFormat.Time => _value.ToString("T", CultureInfo.CurrentCulture),
DateTimePickerFormat.Custom when !string.IsNullOrEmpty(_customFormat) => _value.ToString(_customFormat, CultureInfo.CurrentCulture),
_ => _value.ToString("D", CultureInfo.CurrentCulture),
};
public event EventHandler? ValueChanged;
public event EventHandler? CloseUp;
public event EventHandler? DropDown;
protected virtual void OnValueChanged(EventArgs e) => ValueChanged?.Invoke(this, e);
/// <summary>浏览器里选了新的日期或时间(ISO 格式,本地时间):和 WinForms 一样先 ValueChanged,再 CloseUp</summary>
protected override void OnUserAction(UserAction action)
{
switch (action.Action)
{
case "dropDown":
DropDown?.Invoke(this, EventArgs.Empty);
break;
case "pick" when DateTime.TryParse(action.Value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var picked):
var clamped = picked < _minDate ? _minDate : picked > _maxDate ? _maxDate : picked;
if (clamped != _value) Value = clamped;
else NotifyValue();
CloseUp?.Invoke(this, EventArgs.Empty);
break;
default:
base.OnUserAction(action);
break;
}
}
private void NotifyValue()
{
NotifyChanged("Text", Display);
NotifyChanged("Value", _value.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
}
protected override void WriteProps(JsonObject props)
{
base.WriteProps(props);
props["Value"] = _value.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
props["Format"] = _format.ToString();
props["ShowUpDown"] = _showUpDown;
props["HasTime"] = _format == DateTimePickerFormat.Time || (_format == DateTimePickerFormat.Custom && (_customFormat ?? "").IndexOfAny(['H', 'h', 'm', 's']) >= 0);
props["HasDate"] = _format != DateTimePickerFormat.Time && !(_format == DateTimePickerFormat.Custom && (_customFormat ?? "").IndexOfAny(['y', 'M', 'd']) < 0);
}
}
+1
View File
@@ -98,3 +98,4 @@ public interface IButtonControl
void NotifyDefault(bool value);
void PerformClick();
}
public enum LeftRightAlignment { Left, Right }
+2
View File
@@ -13,6 +13,8 @@ public static class PropValue
public static JsonNode? From(object? value) => value switch
{
null => null,
// 控件自己组织好的结构(如 DataGridView 的列和行)原样发送
JsonNode node => node.Parent == null ? node : node.DeepClone(),
string s => JsonValueOf(s),
bool b => JsonValueOf(b),
char c => JsonValueOf(c == '\0' ? "" : c.ToString()),
+5
View File
@@ -13,6 +13,9 @@ public sealed class UserAction
public string Button { get; init; } = "left";
public int X { get; init; }
public int Y { get; init; }
/// <summary>DataGridView 的单元格:行号、列号(-1 表示行标题)</summary>
public int Row { get; init; } = -1;
public int Column { get; init; } = -1;
public static UserAction FromJson(JsonObject json) => new()
{
@@ -22,5 +25,7 @@ public sealed class UserAction
Button = (string?)json["button"] ?? "left",
X = (int?)json["x"] ?? 0,
Y = (int?)json["y"] ?? 0,
Row = (int?)json["row"] ?? -1,
Column = (int?)json["column"] ?? -1,
};
}