给排班系统做一次工程化体检——8 项整改从发现到落地

举报
哦啦啦啦啦 发表于 2026/08/26 13:10:38 2026/08/26
【摘要】 关键词:Python Flask、排班算法、工程化整改、自动化测试、华为云开发者作品展览馆 起点:一个能跑但不够整洁的项目今天看继续改我的[值班排班管理系统],一个基于 Python Flask 的 Web 应用,用来解决固定班底全年值班排班的公平性问题。项目本身不大,核心就两个文件:app.py — Flask 后端,路由 + 内存缓存scheduler.py — 排班算法,真正的"大脑"...

关键词: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.pyscheduler.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 里的口头承诺"变成"可执行的断言",以后改代码有底气。缓存是从"能跑"到"长期稳定能跑"的区别。

其余几项都是几分钟的小事,但加在一起就是"这个项目能不能让人放心读"的差别。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。