课时统计 图标

课时统计

CourseHours

点名一次,课时、欠费和这个月的账一起算清楚

v0.3.0macOS (Apple Silicon)Tauri 2React 19rusqlite个人项目,按原样提供
课时统计的「今日」界面。左边列着今天的两节课,各自标注人数、第几次课和本次预计扣多少课时,其中一节有两人扣后为欠课时;右边是课时预警名单和本月概览,金额显示为星号
今日 · 金额默认隐藏,要看得先过一次 Touch ID

为什么会有它

一位老师带五个左右的英语小班,每班不到十个孩子,各班在一周的不同时间上课。学生按课次买课包, 上课点名扣课时。这些事本身不难,难的是账:谁还剩几节课、谁已经上成了负数、这个月收了多少钱、 课消了多少 —— 平时散在微信记录、纸质名册和一张越改越乱的表格里,月底想对一次要花一晚上。

于是做了这么一个东西。一台 Mac,一个人用,不登录、不联网、没有家长端。 点名是唯一的入口:点完名,课时、欠费、出勤率、当月课消收入同时就位。

账本只追加,余额是算出来的

数据库里没有「余额」这个字段。余额是 ledger_entry 这张表里所有分录的 SUM(delta)。

点名扣课时、充值、抵扣、调整,全都是往表里 INSERT 一行。点错了要撤销,也不删不改, 而是写一条方向相反的 adjust 分录把它冲掉。历史行永远保持原样。

这条规矩换来两件事:任何一个数字都能顺着流水倒推回它是怎么来的;以及,不会出现 「余额字段和流水对不上,但不知道哪边错了」这种没法收场的局面。设置页有个「校验账本一致性」按钮, 把 spec 里写的每条不变量逐条跑一遍。

几个不打算「优化」掉的产品判断

这类系统很容易越做越像财务软件,所以有几条是一开始就定死的:

  • 缺勤不扣课时,但请假和缺勤分开记。 扣不扣是钱的事,两者都是零;分开记是人的事 —— 出勤率统计和「要不要找家长聊一聊」不是同一个问题。
  • 课包不设有效期。 用完了还能继续上,超出的部分记成欠课时,余额允许为负。 下次充值时用新买的课次抵扣掉。
  • 单价不是设置项。 每笔充值填的是「课次数 + 总金额」,单价当场算出来、逐包保存。 同一个学生不同时期的课包可以是不同价钱,扣课时按 FIFO 从最早的包里结转, 扣多少、按哪个包的单价,都在写入时快照下来。之后改设置不回溯历史。
  • 未消耗课时和欠课时不互相抵消。 报表里分成两行列:一边是你欠家长的课(预收), 另一边是家长欠你的钱。算成净额会把两个问题一起藏起来。
  • 取消一次课就是少上一次,不往后顺延。

排课规则只是计划

「周二、周四 14:00–15:30,A 教室」这种循环规则本身不产生任何课,它只负责按周批量生成课次。 生成完两者就脱钩了:改规则不会动已经生成的课,改某一次课也不会反向修改规则。

课表是周视图,单次取消和临时加课都在上面直接做,拖拽按 15 分钟吸附。 至于法定节假日 —— 不自动跳过,手动取消那一周就好,这是有意留的口子。

金额默认是打码的

侧边栏有个「显示金额 / 隐藏金额」,默认隐藏,所有金额显示成 *****。 想看要先过一次系统验证(Touch ID,没有就用登录密码),验证后当天有效,隔天自动回到隐藏。

验证这件事 WebView 里做不了,是一个用 LocalAuthentication 写的十几行 Swift 小程序, 编译成 sidecar 随应用一起打包,pnpm tauri dev / build 之前自动编一次。

技术上值得一提的

前端是 React + TypeScript,十二屏,一屏一个文件;不引 UI 组件库,也不引图表库 —— 报表里的柱状图是手写的 SVG。样式变量从一份 design tokens 文档生成,暖米白底配赤陶色, 是照着设计稿一个个色值抄过去的,不凭印象重画。

业务规则(扣课时、FIFO 选包、充值抵扣、排课展开)写成 Rust 纯函数,带单测; 点名、充值、撤销都是 SQLite 事务。前端测试把 Tauri 的 invoke 换成假数据, 一层冒烟(十二屏加主要弹窗真渲染一遍,断言没有 console.error), 一层交互(断言发给后端的参数是对的)。加了新命令忘了补假数据,测试会直接报出来。

自动更新还是手写的:Tauri 官方的 updater 插件要求给更新包签名,个人项目没有那套证书, 于是自己读 GitHub Releases API —— 下载 zip、解压、退出后原地替换 .app、重启。 替换脚本先把旧版改名备份,复制成功才删,失败回滚。

安装和数据

没有经过 Apple 公证,第一次打开会提示「已损坏」。在终端跑一次就好:

xattr -dr com.apple.quarantine "/Applications/课时统计.app"

之后应用内更新的版本不用再跑,替换脚本会顺手清掉隔离属性。

数据库在 ~/Library/Application Support/com.coursehours.app/data.db, 同目录下 backups/ 每晚自动 VACUUM INTO 一份,也可以在「设置 → 数据与备份」里 手动备份、恢复或导出。没有云,没有账号,删掉这个目录就什么都不剩了。

← 回到全部产品