Skip to content

feat: 支持退选多门替换课程(replaceCodes 字段) - #27

Open
WONDER-y8 wants to merge 1 commit into
ceilf6:masterfrom
WONDER-y8:feat/multi-replace-codes
Open

WONDER-y8 wants to merge 1 commit into
ceilf6:masterfrom
WONDER-y8:feat/multi-replace-codes

Conversation

@WONDER-y8

Copy link
Copy Markdown

变更内容

原来的换课功能 replaceCode 只支持退选一门课,本 PR 将其扩展为支持多门。

  • 新字段 replaceCodes: string[](数组),取代原 replaceCode: string
  • 发现目标课有余量时,依次退选 replaceCodes 中所有课程,全部成功后再抢课;任意一门失败则中止本次
  • UI 输入框支持逗号分隔多个课程号,与 timeFilter/teacherFilter 风格一致
  • README 同步更新换课说明与代码示例

使用场景

想选 A 课,但需要同时腾出两门课的时间位置:

```javascript
{
code: '23306049',
priority: 1,
replaceCodes: ['23306047', '23306050'] // 依次退选这两门,再抢目标课
}
```

UI 面板替换课程输入框填写:23306047,23306050

测试

  • 单门替换(向下兼容,填一个课程号正常工作)
  • 多门替换(填两个及以上课程号,依次退选后抢课)
  • 任意一门退选失败时中止,不触发选课

🤖 Generated with Claude Code

- 新字段 replaceCodes: string[],取代原 replaceCode: string
- 发现目标课有余量时依次退选所有替换课程,全部成功后再抢课
- 任意一门退选失败则中止本次,等待下次轮询
- UI 输入框支持逗号分隔多个课程号,与 timeFilter/teacherFilter 风格一致
- README 同步更新换课说明与代码示例
@ceilf6

ceilf6 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

@ceilf6/repo-guard

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🛡️ ceilf6/repo-guard

代码评审报告: feat: 支持退选多门替换课程(replaceCodes 字段)

风险等级:
处理建议: 请求修改
决策摘要: 不建议现在合并——多门退选是非原子操作,任意一门失败会永久丢失已退课程且后续重试必然再次失败(目标课再也抢不到),同时 replaceCodereplaceCodes 的配置契约破坏没有兼容处理。

级联分析

  • 变更符号: attemptGrabSingleCourse 内的替换分支(courseGrabber.js:1705-1752)、cg-add-course 点击处理、updateCourseListwindow.editCourseUI;配置字段 TARGET_COURSES[].replaceCodereplaceCodes
  • 受影响流程: 换课抢课主循环 → dropCourseselectTeachingClass;UI 添加/展示/编辑课程;用户手写的 TARGET_COURSES 代码配置(README 公开契约)
  • 变更集外调用方: 仓库内 grep replaceCode 已无遗留引用(text search,全仓库确认);dropCourse(courseGrabber.js:1046)、safeParseFilterInput(courseGrabber.js:164) 签名未变。仓库外的调用方是用户已写好的 replaceCode: '...' 配置,脚本升级后该字段被静默忽略,换课行为直接消失且无任何日志提示
  • 无本地持久化:未使用 localStorage/JSON 存储(text search 确认),因此不存在已保存配置的迁移问题
  • 置信度: medium(单文件脚本,全部仓库内引用已由文本搜索确认更新;无代码图谱,且外部用户配置无法遍历)

问题发现

  1. [高] 部分退选成功后中止会永久丢失课程,并让该目标课进入无法恢复的死锁

    • 证据: courseGrabber.js:1723-1729 顺序退选,if (!ok) return Promise.resolve(false) 直接中止,不回滚已成功退掉的课程。下一轮 tick 重新进入该分支时,dropCourse(codes[0]) 会在已选列表和教学班中都找不到退选按钮(courseGrabber.js:1105-1132 → resolve(false)),于是链条永远在第一门就返回 false
    • 受影响调用方/流程: 换课主流程;用户的保底课已被退掉,目标课从此再也不会被选,state.selecting 每轮复位后重复失败
    • 最小可行修复: 退选前先判断该课是否仍在已选列表(复用 findSelectedCourseRows),"不在已选列表" 视为该门已完成而非失败;失败分支的日志要列出已成功退掉哪几门,让用户能立即手动补救
  2. [高] 多门退选期间未置 state.selecting,并发 tick 会重复触发整段退选序列

    • 证据: courseGrabber.js:1709 进入替换分支后直到 1743 的 selectTeachingClass 才会置 state.selecting = true(courseGrabber.js:1279)。CHECK_INTERVAL = 1000(courseGrabber.js:116),而单门退选耗时约 800+1500ms(courseGrabber.js:1150/1238)再加 1500ms 等待,N 门退选把这个未加锁窗口从约 3.8s 拉长到 N×2.3+1.5s
    • 受影响调用方/流程: attemptGrabCourse 并发模式(courseGrabber.js:1819-1822)每秒调用一次 attemptGrabSingleCourse,会并行启动多条 dropSequentially 链,互相点击退选/确认模态框
    • 最小可行修复: 进入替换分支、发起首次 dropCourse 之前置 state.selecting = true,失败分支保留现有复位(这是既有缺陷,但本 PR 把暴露窗口按替换门数线性放大)
  3. [高] replaceCode 配置被静默废弃,无兼容与无校验

    • 证据: courseGrabber.js:1706 只读 courseConfig.replaceCodes;README 此前记录的 replaceCode: '23306047' 写法升级后不报错、不告警,换课直接退化为"不退选直接抢"。另外该字段未做类型归一,用户误写 replaceCodes: '23306047'(字符串)时 courseGrabber.js:1710 的 .join 抛 TypeError,而 attemptGrabSingleCourse 及其调用方(courseGrabber.js:1579、1819)没有 try/catch,整轮抢课会被打断
    • 受影响调用方/流程: 所有沿用旧文档配置的用户;window.editCourseUI(courseGrabber.js:3102) 对旧字段也只显示空值并在保存时丢弃
    • 最小可行修复: 在读取处做一次归一 const codes = normalizeReplaceCodes(cfg)——接受 replaceCodes 数组、字符串,以及旧的 replaceCode,非数组时 [String(x)],同时对命中旧字段打一条 warning 日志提示改名
  4. [中] 顺序退选逻辑内联在教学班循环体内,无法被现有测试覆盖

    • 证据: courseGrabber.js:1723 的 dropSequentially 定义在 for (let tc of teachingClasses) 循环体中,每次迭代重建;tests/courseGrabber-selected-courses.test.mjs 已有 loadDropCourse harness 可单测 dropCourse,但对多门序列无任何等价钩子
    • 受影响调用方/流程: 本 PR 的核心新行为(顺序、短路、部分失败)零测试覆盖,PR 描述中三个测试项全部未勾选
    • 最小可行修复: 把它提到与 dropCourse 同层的具名函数 dropCoursesSequentially(codes),按现有 harness 补顺序/短路/已退课程跳过三个用例
  5. [中] README 未说明部分退选失败的实际后果

    • 证据: README.md:80 只写"任意一门替换课退选失败时本次抢课中止",读者会理解为"无副作用地中止",实际是已退课程不回滚(见发现 1)
    • 受影响调用方/流程: 用户对"保底课"的风险判断——这正是该功能存在的前提
    • 最小可行修复: 明确写出"已退选的课程不会自动选回,需手动恢复",并建议按重要性从低到高排列 replaceCodes

行级发现

  • [courseGrabber.js:1706] 只识别 replaceCodes,旧的 replaceCode 配置被静默忽略;建议在此处归一化(同时接受 replaceCode、字符串形式),并对旧字段输出一次 warning,否则升级用户的换课功能会无声失效。
  • [courseGrabber.js:1709] 进入替换分支后未置 state.selecting = true,1s 的 tick 会在退选序列进行中重复进入本分支并启动第二条退选链;请在发起首次 dropCourse 前加锁。
  • [courseGrabber.js:1710] _replaceCodes 来自用户手写配置,未做数组归一,写成字符串时这里的 .join 会抛 TypeError 并打断整轮抢课(外层无 try/catch)。
  • [courseGrabber.js:1723] dropSequentially 定义在教学班 for 循环体内、每次迭代重建,且无法被 tests/courseGrabber-selected-courses.test.mjs 的 harness 覆盖;建议提为与 dropCourse 同层的具名函数并补顺序/短路用例。
  • [courseGrabber.js:1726] 中途失败直接返回 false,已成功退掉的课程既不回滚也不记录;且下一轮重试时首门已不在已选列表,dropCourse 必然返回 false,导致该目标课永久无法再抢。建议把"已不在已选列表"视为该门完成,并在失败日志中列出已退课程。
  • [courseGrabber.js:1746] 失败日志只说"退选失败",未区分是第几门失败、前面哪几门已经退掉,用户无法据此立即手动补救。
  • [README.md:80] 未说明退选失败时已退课程不会自动选回;请补充这一不可逆后果,并建议用户按重要性排序 replaceCodes

Karpathy 评审

  • 假设: PR 假定"退选序列要么全成功要么无副作用",但 dropCourse 在点击确认后无条件 resolve(true)(courseGrabber.js:1235-1238),既不校验服务端结果,也没有回滚能力;多门串联把这个乐观假设的代价从"丢 1 门"放大到"丢 N-1 门且死锁"。
  • 简洁性: 新增逻辑本身克制,递归的 dropSequentially 可读;但 _replaceCodes/_codesList 的下划线前缀是全文件唯一(text search 确认),与既有命名不一致,建议去掉。
  • 结构质量: 无越界文件膨胀、无薄 wrapper、无重复 canonical helper;唯一结构问题是新逻辑放在循环体内而非与 dropCourse 同层(见发现 4)。
  • 变更范围: 变更集中在替换课程一条链路上,UI 输入解析复用了既有 safeParseFilterInput,与 timeFilter/teacherFilter 风格一致,无无关重构或格式噪声。
  • 验证: 不足。PR 自述的三项测试全部未勾选,仓库内两个测试文件均未覆盖多门退选;dropCourse 已有测试 harness,具备补测条件却未补。

缺失覆盖

  • 顺序退选:两门均成功时按配置顺序调用、且全部成功后才触发 selectTeachingClass
  • 部分失败:第二门失败时不选课,且再次进入同一分支时不会因首门已退而永久返回 false(对应发现 1 的回归用例)
  • 配置兼容:旧 replaceCode 字符串配置、replaceCodes 误写为字符串两种输入不崩溃且行为可预期
  • UI 解析:cg-replace-code 输入 23306047,23306048(含中文逗号/多余空格)后 TARGET_COURSES 中生成正确数组,以及 editCourseUI 往返编辑不丢字段

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants