给排班系统做一次工程化体检——8 项整改从发现到落地
关键词:Python Flask、排班算法、工程化整改、自动化测试、华为云开发者作品展览馆
起点:一个能跑但不够整洁的项目
今天看继续改我的[值班排班管理系统],一个基于 Python Flask 的 Web 应用,用来解决固定班底全年值班排班的公平性问题。
项目本身不大,核心就两个文件:
app.py— Flask 后端,路由 + 内存缓存scheduler.py— 排班算法,真正的"大脑"
算法设计其实挺有亮点:用 K₆ 完全图的 1-factorization(5 个完美匹配轮换)来做轮休配对,每天再按加权贪心评分选值班人——年度均衡权重 100、月度均衡 30、稀疏分布 10,还有个补位奖励 50。2025 年实测 6 人全年均衡差距只有 1,目标 ≤ 2,达标。
但作者自己在 TODO.md 里诚实地列了 8 条工程整洁度问题。功能没问题,安全也没问题,就是些"不影响运行但看着不舒服"的欠账。今天的任务就是:把这 8 条全部清掉。
8 项整改,逐一落地
1. 新增 requirements.txt
原来 README 里只写了 pip install flask,没锁版本。哪天 pip 装了个 Flask 4.0,接口不兼容就炸了。
Flask==3.0.3
一行搞定。小项目也要锁依赖,这是底线。
2. 新增 .gitignore
没有 .gitignore,意味着 __pycache__/、*.pyc、.venv/ 这些垃圾文件随时可能被 git add . 带进仓库。加了一个标准的 Python 项目忽略清单:
__pycache__/
*.pyc
.venv/
.env
*.log
.DS_Store
.vscode/
.idea/
3. 移除 # -*- coding: utf-8 -*-
app.py 和 scheduler.py 顶部都有这行声明。Python 3 默认源码就是 UTF-8,这行从 Python 3.0 开始就是多余的。删掉,少一行噪音。
# 删前
# -*- coding: utf-8 -*-
"""
值班排班管理系统 - Flask 后端主程序
"""
# 删后
"""
值班排班管理系统 - Flask 后端主程序
"""
4. 删除死代码 generate_month_schedule
scheduler.py 末尾有个 generate_month_schedule 便捷函数,全项目没有任何地方调用它。死代码不只是占行数——它会误导后来者以为这是公开 API,不敢删,越积越多。
# 整个函数删除,不留注释占位
def generate_month_schedule(year, month, ...):
... # ← 无人调用,已删除
删完后 scheduler.py 从 250 行减到 236 行,import 验证通过。
5. 清理 readPeopleInputs 的幽灵参数
前端 app.js 里有个函数:
// 整改前
function readPeopleInputs(container, expectedCount) {
// expectedCount 从未被使用
const inputs = container.querySelectorAll('.people-input');
...
}
// 两处调用点
const fixed = readPeopleInputs(fixedPeopleInputs, 6); // 6 传了但没用
const extra = readPeopleInputs(extraPeopleInputs, 2); // 2 传了但没用
expectedCount 传进去了,函数体里压根没引用。这种参数比没有更糟——它暗示了一个不存在的校验逻辑。删掉参数,同步修改两处调用点:
// 整改后
function readPeopleInputs(container) { ... }
const fixed = readPeopleInputs(fixedPeopleInputs);
const extra = readPeopleInputs(extraPeopleInputs);
6. 给 schedule_cache 加 LRU 上限
这是最有实际影响的一项。原代码:
schedule_cache = {}
缓存按 (年份, 固定人员, 额外人员) 做 key,只增不删。长期运行下,不同年份 × 不同人员组合会无限增长——虽然这是个值班排班工具不太会跑很久,但"只增不删的缓存"是个坏习惯。
改成 OrderedDict 实现 LRU,上限 50 条:
from collections import OrderedDict
CACHE_MAX_SIZE = 50
schedule_cache = OrderedDict()
def _cache_put(key, value):
"""写入缓存并维护 LRU 上限"""
if key in schedule_cache:
schedule_cache.move_to_end(key)
schedule_cache[key] = value
while len(schedule_cache) > CACHE_MAX_SIZE:
schedule_cache.popitem(last=False) # 淘汰最久未访问
def _cache_get(key):
"""读取缓存,命中时移到末尾(标记为最近访问)"""
if key in schedule_cache:
schedule_cache.move_to_end(key)
return schedule_cache[key]
return None
不只是"超出就删"——访问时也 move_to_end,频繁查询的年份不会被误淘汰,符合 LRU 语义。
7. 年份选择器动态化
index.html 里硬编码了 2024-2027 四个选项:
<!-- 整改前 -->
<select id="yearSelect">
<option value="2024">2024 年</option>
<option value="2025">2025 年</option>
<option value="2026">2026 年</option>
<option value="2027">2027 年</option>
</select>
JS 里有个 initYearSelect() 会补上当前年份,但手动选择范围还是被限死了。改成 JS 动态生成当前年 ±3 年:
// 整改后
function initYearSelect() {
yearSelect.innerHTML = '';
for (let offset = -3; offset <= 3; offset++) {
const y = currentYear + offset;
const opt = document.createElement('option');
opt.value = y;
opt.textContent = y + ' 年';
yearSelect.appendChild(opt);
}
yearSelect.value = String(currentYear);
}
2026 年会显示 2023-2029,范围更宽且永远包含当前年。
8. 新增自动化测试
README 声称"2025 年 6 人均衡差距 ≤ 2 达标",但没有可重复的断言。如果算法改了一行,你怎么知道还是不是达标?
用 Python 标准库 unittest(不引入额外依赖),写了 8 个测试:
class TestScheduleBalance(unittest.TestCase):
def test_balance_gap_within_target(self):
"""固定 6 人全年均衡差距应 <= 2"""
result = generate_schedule(2025)
self.assertLessEqual(result['balance_gap'], 2)
class TestDailyAssignment(unittest.TestCase):
def test_every_day_has_duty_person(self):
"""每月每天恰好 1 人值班"""
for month_data in self.result['months']:
for day_info in month_data['days']:
self.assertIsNotNone(day_info['duty_person'])
def test_resting_people_not_on_duty(self):
"""轮休人当天不出现在值班人里"""
for month_data in self.result['months']:
for day_info in month_data['days']:
self.assertNotIn(day_info['duty_person'],
day_info.get('resting', []))
三条核心断言 + 五条辅助检查(值班人合法性、额外人员仅 9-12 月参与、月天数与日历一致等),一次跑完 0.05 秒:
test_balance_gap_within_target ... ok
test_fixed_people_have_similar_total ... ok
test_every_day_has_duty_person ... ok
test_duty_person_is_valid ... ok
test_resting_people_not_on_duty ... ok
test_extra_people_only_in_sep_to_dec ... ok
test_month_days_match_calendar ... ok
test_year_has_12_months ... ok
Ran 8 tests in 0.053s — OK
提交并推送
8 项全部完成,git add -A 后 8 个文件变更(3 新增 + 5 修改),207 行增、59 行删:
git commit -m "工程化整改:完成 TODO.md 全部 8 项"
git push origin main
提交 50d9e3b 推送至 GitCode main 分支,一次通过。
回顾
| 整改项 | 耗时 | 难度 |
|---|---|---|
| requirements.txt | 1 分钟 | ⭐ |
| .gitignore | 1 分钟 | ⭐ |
| 移除 coding 声明 | 1 分钟 | ⭐ |
| 删除死代码 | 2 分钟 | ⭐ |
| 清理幽灵参数 | 3 分钟 | ⭐⭐ |
| LRU 缓存 | 10 分钟 | ⭐⭐⭐ |
| 年份选择器动态化 | 5 分钟 | ⭐⭐ |
| 自动化测试 | 15 分钟 | ⭐⭐⭐ |
8 项里最值得花时间的是测试和缓存。测试是把"README 里的口头承诺"变成"可执行的断言",以后改代码有底气。缓存是从"能跑"到"长期稳定能跑"的区别。
其余几项都是几分钟的小事,但加在一起就是"这个项目能不能让人放心读"的差别。
- 点赞
- 收藏
- 关注作者
评论(0)