在这里插入图片描述
在这里插入图片描述

实例:课程表管理(Course)|技术:二维坐标字段、冲突检测、周维度

一、业务需求分析:课程表的数据形态

课程表是**「二维网格」数据**——每门课占据「第几周几的第几节」这个坐标。这与前面的列表型数据(待办、便签)截然不同:数据是坐标定位的(weekday × section),不是顺序排列的。

核心业务需求:

  1. 课程记录:课程名、老师、教室、周几(17)、节次(18)、色块颜色、教学周;
  2. 二维网格展示:周几 × 节次的课表——像真实课程表一样按格子摆放;
  3. 冲突检测:同一天同一节次只能有一门课——新增前检测冲突;
  4. 按天查询:看周一/周二…的课表与课程列表;
  5. 周切换:一周 7 天的横向切换。

二、字段设计:坐标式课程

课程表 course

字段名 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
name TEXT NOT NULL 课程名
teacher TEXT DEFAULT ‘’ 老师
location TEXT DEFAULT ‘’ 教室
weekday INTEGER NOT NULL 周几 17(周一周日)
section INTEGER NOT NULL 节次 1~8
color TEXT DEFAULT ‘#3B82F6’ 色块底色 HEX
weeks TEXT DEFAULT ‘1-16’ 教学周(‘1-16’ 或 ‘1,3,5’)
remark TEXT DEFAULT ‘’ 备注

设计要点拆解

1. weekday + section 是二维坐标(weekday, section) 二元组唯一确定课表中的一个格子——这是课程表的空间语义:不是「时间先后」而是「网格位置」。查询「周一第 3 节有没有课」就是 WHERE weekday=1 AND section=3

2. 为什么用数字 1~7 / 1~8 而不是字符串? weekday 参与等值筛选(equalTo('weekday', 1))与排序(orderByAsc('weekday')),数字编码让筛选与排序直接可靠;界面显示「周一~周日」由 days = ['一','二','三','四','五','六','日'] 数组映射——数字存储 + 数组映射展示

3. color 用 HEX 字符串。每门课一个色块颜色(10 色板 COURSE_COLORS)——课表的视觉核心(色块区分课程)。存 HEX 字符串,页面直接做 backgroundColor。展示性字段存「最终形态」(HEX 而非颜色名枚举)——不需要参与筛选计算。

4. weeks 教学周用字符串。‘1-16’(连续周)或 ‘1,3,5’(隔周)——真实教务系统的教学周表达。Demo 存字符串不解析(展示用),进阶可解析成周集合做「本周是否上课」判断。

5. 连堂课程是两条记录。高等数学周一 1、2 节连堂——种子里是两条 section=1section=2 的记录(同 name/teacher/location/color)。「连堂 = 相邻节次的多条记录」:查询时按节次分组渲染,同色相邻自然形成「连堂块」。这是课程表的通用建模(不用「起止节次」字段,因为单节也是记录、网格填充更简单)。

三、建表 SQL 与索引

CREATE TABLE IF NOT EXISTS course (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  teacher TEXT DEFAULT '',
  location TEXT DEFAULT '',
  weekday INTEGER NOT NULL,
  section INTEGER NOT NULL,
  color TEXT DEFAULT '#3B82F6',
  weeks TEXT DEFAULT '1-16',
  remark TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_course_grid ON course (weekday, section);

idx_course_grid (weekday, section) 联合索引WHERE weekday=1 AND section=3(冲突检测)与 WHERE weekday=1 ORDER BY section(按天查询)都命中——二维坐标的联合索引是「坐标查询」的性能保障(两个条件同时过滤)。

四、CourseDao 封装:冲突检测是亮点

数据层核心 CourseDao,本实例的独特方法是冲突检测 checkConflict

/** 冲突检测:同一天同一节次是否已有课 */
static async checkConflict(context: common.Context, weekday: number, section: number, excludeId: number): Promise<boolean> {
  const store = await CourseDao.getStore(context);
  const predicates = new relationalStore.RdbPredicates(CourseDao.TABLE);
  predicates.equalTo('weekday', weekday).equalTo('section', section);
  if (excludeId > 0) {
    predicates.notEqualTo('id', excludeId);
  }
  const result = await store.query(predicates);
  const has = result.goToNextRow();
  result.close();
  return has;
}

冲突检测的本质:查「同坐标是否存在记录」——weekday=1 AND section=3 查到任何一行即冲突。excludeId 排除自身:编辑课程换时间段时,排除当前课程自己(否则自己占的格子永远「冲突」)——notEqualTo('id', excludeId) 让编辑不误报。

为什么冲突检测在数据库层做? 虽然表结构没加 UNIQUE(weekday, section)约束(编辑换时间需要先删约束逻辑),但应用层检测 + 提示比裸约束友好——页面拿到 conflict=true 弹 Toast「该时段已有课程,冲突!」,用户知道为什么失败。**「检测-提示-阻止」**比「违反约束-报错」体验好得多。

其他方法族(与前面实例同构):

  • queryAll:全部课程按 weekday、section 升序(课表数据源);
  • queryByDay:某天课程按 section 升序(当天列表);
  • insert/update/delete:标准 CRUD(update 全量);
  • count:课程总数(标题栏「共 14 门课程」)。

五、节次时间表与色板常量

/** 节次时间表(第几节 → 起止时间) */
export const SECTION_TIMES: string[] = [
  '08:00-08:45', '08:55-09:40', '10:00-10:45', '10:55-11:40',
  '14:00-14:45', '14:55-15:40', '16:00-16:45', '16:55-17:40',
];

/** 课程色板 */
export const COURSE_COLORS: string[] = [
  '#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6',
  '#06B6D4', '#EC4899', '#84CC16', '#F97316', '#6366F1',
];

SECTION_TIMES:节次时间表(第 1 节 08:00-08:45…)——展示层显示「第 3 节 10:00-10:45」用。「坐标 → 人类可读」的映射表,与 days 数组同理。

COURSE_COLORS:10 色课程色板——新增课程时选色(色块圆形选择器),课表渲染用。颜色是课程的视觉标识(扫一眼色块认课程)。

六、技术要点对照表

技术点 实现方式 生产价值
二维坐标 weekday + section 字段 网格定位
联合索引 idx_course_grid (weekday, section) 坐标查询加速
冲突检测 同坐标查重 + excludeId 防排课冲突
数字枚举 weekday 1~7 / section 1~8 筛选排序可靠
色块字段 color HEX 字符串 视觉区分
连堂建模 相邻节次多条记录 网格填充简单

七、文章小结

课程表的数据层核心是**「二维坐标建模 + 冲突检测」**:weekday × section 二元组定位课表格子(空间语义而非时间顺序),联合索引加速坐标查询,checkConflict(同坐标查重 + excludeId 排除自身)在应用层做「检测-提示-阻止」的友好冲突处理。数字枚举(17/18)保证筛选排序可靠,色块 HEX 与节次时间表是展示层的映射资源。这是「网格/排班类应用」的数据模型范式。

下一篇(15-2)展示课表网格 UI——周切换条 + 节次时间轴 + 色块课表 + 当天课程列表。

八、字段设计详解:从「存什么」到「怎么用」

8.1 weekday × section:一张 7×8 的坐标纸

课程表本质是一张 7 行(周一~周日)× 8 列(第 1~8 节)= 56 个格子的坐标纸,(weekday, section) 就是格子的坐标。这一设计带来三个直接收益:

  1. 定位语义直接:查询「周三第 4 节有没有课」直接翻译为 WHERE weekday=3 AND section=4——坐标即条件,无需范围运算;
  2. 排序即布局ORDER BY weekday, section 的输出顺序天然等于「从左到右、从上到下」扫描课表——页面渲染零重排;
  3. 网格与列表双视图:按天列表 WHERE weekday=3 是同坐标过滤的特例,一套字段支撑两种 UI。
/** 坐标 → 页面网格位置:第 weekday 行第 section 列 */
function gridCell(weekday: number, section: number): string {
  return `${days[weekday - 1]} · 第${section}`; // '周三 · 第4节'
}

7×8 网格示意(第 1~5 节):

节次 \ 周几 周一 周二 周三 周四 周五
第1节 高等数学 数据结构 大学物理
第2节 高等数学 数据结构 大学物理
第3节 大学英语 操作系统
第4节 大学英语 操作系统
第5节 体育

8.2 数字枚举 vs 字符串:为什么存 1 而不是「周一」

方案 存储示例 筛选 排序 界面展示
数字枚举 weekday=1 equalTo('weekday', 1) 直接命中索引 orderByAsc 数字序即周序 days 数组映射「周一」
字符串 weekday=‘周一’ 等值可筛但易写错(‘周1’/‘一’) 字典序「周二」在「周一」前 无需映射

核心结论:weekday 是参与筛选与排序的键,必须用稳定、可比较的编码(数字);「周一」是参与展示的语义,交给映射数组转换。「存储用编码、展示用映射」——展示层不承担存储负担,存储层不承担展示格式。

/** 展示层:数字 → 中文 */
const days: string[] = ['一', '二', '三', '四', '五', '六', '日'];
const label: string = `${days[course.weekday - 1]}`; // weekday=1 → '周一'

8.3 color 色块与 weeks 教学周:两个「展示形态」字段

  • color(HEX 色块):课表是色块驱动的视觉界面——扫一眼颜色认课程。存 #3B82F6 这类最终形态,页面直接 backgroundColor(course.color) 渲染,无需颜色名 → HEX 二次转换。它不参与筛选计算,所以不做枚举约束——HEX 本身就是精确值。
  • weeks(教学周)'1-16'(连续周)与 '1,3,5'(隔周)两种表达,来自真实教务系统。Demo 层存字符串不做解析(仅展示「1-16 周」);进阶做法是把 weeks 解析成周集合,判断「本周是否上课」:
/** 进阶:'1,3,5' → {1,3,5},判断第 6 周是否上课 */
function isWeekIn(weeks: string, week: number): boolean {
  if (weeks.includes('-')) {
    const [s, e] = weeks.split('-').map(Number);
    return week >= s && week <= e;          // 连续周:区间判断
  }
  return weeks.split(',').map(Number).includes(week); // 隔周:集合判断
}

九、联合索引 idx_course_grid:二维坐标的加速器

CREATE INDEX IF NOT EXISTS idx_course_grid ON course (weekday, section);

B+ 树按 (weekday, section) 两键有序,把 56 个格子组织成「先周后节」的线性序。命中场景:

查询 SQL 形态 是否命中索引
冲突检测 WHERE weekday=1 AND section=3 ✅ 双键等值,最优
按天列表 WHERE weekday=1 ORDER BY section ✅ 前缀等值 + 有序扫描
课表全量 ORDER BY weekday, section ✅ 索引即排序,免额外排序

为什么是联合索引而不是两个单列索引? 单列 (weekday) 索引按天查询也快,但冲突检测要同时过滤两个条件,两个单列索引需要「索引合并 + 回表」;联合索引一次 B+ 树定位直达叶子——weekday=1 AND section=3 只走树的一条路径。数据量小时(14 门课)索引优势不明显,但这是「网格类数据」的通用范式:坐标查询天然是双键过滤,联合索引从第一天就该建好。

十、连堂课程:相邻多记录建模

问题:高等数学周一 1、2 节连堂,怎么存?

方案 存储 优点 缺点
起止节次 section_start=1, section_end=2 一条记录 单节课要冗余 start=end;网格填充要展开区间;冲突检测要区间相交判断
相邻多记录 section=1section=2 两条 单节/连堂统一建模;每格一条记录、网格直接填;冲突检测仍是点查询 记录数略多(14 门课约 16 条)

「相邻多记录」是网格应用的通解:连堂 = 同 name/teacher/location/color 的相邻节次记录集合。渲染时同色相邻自动成块

/** 判断某课是否与上一节同课(连堂块的延续) */
function isConsecutiveBlock(courses: Course[], index: number): boolean {
  const cur = courses[index];
  const prev = courses[index - 1];
  return prev !== undefined && prev.section === cur.section - 1 &&
         prev.name === cur.name && prev.color === cur.color;
}

数据已按 ORDER BY weekday, section 排序,同一门连堂课的记录必然相邻,name + color 相同即判定为同一连堂块——数据层无额外逻辑,展示层自动合并成块(圆角、hover 状态只画在块边界)。

十一、冲突检测思路预览

checkConflict 的完整实现将在 15-3 深入,这里先看整体思路:

  1. 查重WHERE weekday=1 AND section=3——同坐标存在记录即冲突;
  2. 排除自身:编辑课程换时段时用 excludeId 排除当前课程 id——自己占的格子不算冲突;
  3. 检测-提示-阻止:页面拿到 conflict=true 弹 Toast「该时段已有课程,冲突!」,阻止保存。
场景 excludeId 结果
新增课程,格子已被占 0(不排除) 冲突,阻止
编辑课程,改到空时段 自身 id 不冲突,放行
编辑课程,改到他人时段 自身 id 冲突,阻止

十二、FAQ

Q1:为什么不用 UNIQUE (weekday, section) 约束直接防冲突?
A:UNIQUE 是数据库硬约束,冲突时抛异常,用户只看到「保存失败」,不知道为什么失败。应用层 checkConflict 能给出「该时段已有课程,冲突!」的友好提示,且编辑换时段可通过 excludeId 灵活放行——「检测-提示-阻止」优于「违反-报错」

Q2:weekday 为什么从 1 开始而不是 0?
A:与业务语义对齐——周一到周日就是 1~7,days[weekday-1] 映射直观;0 起是程序员习惯,容易和「0 代表周日」的歧义混淆。

Q3:课程表为什么不做「按周」维度(单双周课)?
A:Demo 用 weeks 字符串标记教学周(展示用),未做周维度解析。真实教务系统需要「周 × 周几 × 节次」三维坐标,进阶可在 weeks 解析成集合后按周过滤。

Q4:连堂课为什么不在表里加「连堂组 id」字段?
A:相邻多记录 + 同色判断已能自动成块,加组 id 反而引入额外约束(组内节次必须连续、不能拆开编辑)。数据层越简单,渲染越自由。

Q5:色块颜色存 HEX 还是颜色名?
A:存 HEX 最终形态(页面直接 backgroundColor),不做颜色名枚举——颜色不参与筛选计算,存「最终形态」省一次转换、少一层映射表。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐