先直接说结论。这个项目就是用 Python 的 Flask 框架做一套网页版环卫工人管理系统,核心功能是:环卫工人的档案登记、排班考勤、绩效打分和工资汇总。管理员在浏览器里就能完成日常操作,不用再抱着 Excel 表格换来换去。
我之所以想把整个项目完整复盘一遍,是因为它看起来只是一个普通的信息管理系统,但真正开发的时候会碰到很多教科书不会写清楚的问题。比如:工人编号怎么设计才稳定?考勤数据按人存还是按天存?绩效打分跟主观评价怎么共存?这些问题的答案直接决定系统好不好用。如果你正在做 Flask 相关的管理类项目、毕业设计,或者想把 Python Web 开发基础完整过一遍,这篇文章可以给你一条可以照着走的参考路径。
1. 环卫管理场景的真实需求:不是简单做一张工人花名册
1.1 一线管理部门遇到的典型管理问题
环卫工人管理有个很大的特点:人数量大、流动性高、排班变动频繁。我调研了几个环卫所的实际工作流程,发现他们日常最消耗精力的事情,集中在下面几件:
- 入职离职登记。一线工人经常因为各种原因离职,新工人补岗也很快,如果靠纸质登记表,序号很容易乱,人员档案一多就没法查。
- 排班调整。清扫路段、倒班时间、节假日加班,这些调整往往通过微信群通知,信息散落各处,月底核对时全靠聊天记录翻。
- 考勤统计。每天谁出勤、谁请假、谁代班,没有统一录入入口,月底汇总要多个人人工比对。
- 绩效与工资计算。环卫工的工资绩效跟出勤率、路段检查评分、投诉处罚挂钩,人工算非常容易出错。
这些需求放到一起,本质上就是一套以“人”为核心的管理系统。所以我在设计系统时没有一上来就堆功能,而是先明确了最重要的功能边界,也就是下面这张表:
| 功能模块 | 要解决什么问题 | 优先级 |
|---|---|---|
| 工人档案 | 基础信息电子化,支持快速检索 | 高 |
| 排班管理 | 按路段/时段安排工作,支持临时调整 | 高 |
| 考勤打卡 | 每日出勤情况记录与统计 | 高 |
| 绩效记录 | 打分与原因留痕,方便月底核算 | 中 |
| 数据看板 | 用图表展示出勤率、人员分布等 | 低(后期加分) |
1.2 为什么用 Python + Flask 而不是更重的框架
对于这种规模的系统,完全没必要上 Django 或者 Spring Boot。Flask 的轻量特性正好匹配:项目结构简单、开发效率高、本地部署方便,一个虚拟环境加一个 Python 文件就能跑起来。更重要的是,Flask 的扩展生态足够覆盖本项目需求:Flask-SQLAlchemy 管数据库,Flask-WTF 管表单,Jinja2 管页面渲染,全部都是成熟方案。
如果项目需要公开上线、用户量大,那我会建议换 Django 或者上前后端分离架构。但环卫管理系统属于典型的“内部使用、低并发、低成本维护”,Flask 是性价比最高的选择。这也是很多政务基层场景偏爱它的原因——不是因为它最热门,而是因为它够用且不复杂。
1.3 从原始需求到功能清单的转化
我习惯先用一句话把系统定位说清楚:给环卫管理人员提供一个统一操作的网页后台,替代纸质台账和 Excel 大表。基于这句话,功能清单可以拆成:
- 登录注册,管理员身份验证;
- 工人档案的增删改查,支持姓名、路段、状态筛选;
- 排班管理,按路段分配工人,支持调整备注;
- 每日考勤登记,记录出勤、请假、缺勤;
- 绩效打分,按出勤和日常表现打分并备注原因;
- 数据统计页,展示出勤率、各路段人数分布等内容。
这里我要强调一点:功能清单不是写完就固定的。我在开发过程中跟使用者聊完以后,把“工资计算”从核心功能挪到了后续迭代里,因为工资涉及补贴、扣款等复杂规则,第一版强行做很容易做错。先保证核心流程跑通,再逐步加复杂规则,这种思路对中小型管理系统特别重要。
2. 项目环境搭建与目录规划:把这个工程立起来
2.1 开发环境配置:Python 版本与虚拟环境
开发环境方面,我推荐 Python 3.10 以上版本。原因是 Flask 2.x 和 SQLAlchemy 2.x 对 Python 3.10 的支持最稳定,且类型注解体验更友好。安装完 Python 后,建议用 venv 创建独立虚拟环境,避免把依赖装到全局环境里:
python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/Mac激活虚拟环境后,再安装依赖:
pip install flask flask-sqlalchemy flask-wtf flask-login四个包就是本项目用到的核心依赖,各自的分工如下:
- flask:Web 框架,负责路由、请求和响应;
- flask-sqlalchemy:ORM,负责把 Python 对象映射到数据库表;
- flask-wtf:表单处理,提供 CSRF 保护和表单验证;
- flask-login:登录用户管理,处理会话和权限控制。
提示:别图省事把依赖装到全局。项目后期换机器、部署到服务器,有一份干净的 requirements.txt 会省掉大量时间。
2.2 Flask 项目目录结构设计
很多人写 Flask 喜欢把代码全部放进一个 app.py,几百行甚至上千行堆在一起。项目只有几十行时无所谓,但一旦要加登录、加排班、加考勤,单文件结构会迅速变成维护噩梦。我把项目拆成下面这样:
huanwei_system/ ├── app.py # 应用入口 ├── config.py # 配置文件 ├── requirements.txt # 依赖清单 ├── models.py # 数据库模型 ├── forms.py # 表单类 ├── views/ # 路由分模块 │ ├── __init__.py │ ├── auth.py # 登录登出 │ ├── worker.py # 工人档案 │ ├── schedule.py # 排班管理 │ ├── attendance.py # 考勤管理 │ └── performance.py # 绩效管理 ├── templates/ # Jinja2 模板 ├── static/ # CSS/JS/图片 └── instance/ # SQLite 数据库文件目录按功能拆分 Blueprint 的好处是:每个模块的代码量不超过 200 行,需要排查问题时直接进对应文件,搜索结果也是模块级别的,不会出现“全局搜索一个函数名要翻三个文件”的情况。
2.3 配置文件的几个关键项
config.py 里的内容不多,但有几个细节值得注意:
import os class Config: SECRET_KEY = os.environ.get("SECRET_KEY") or "dev-secret-key" SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join( os.path.abspath(os.path.dirname(__file__)), "instance", "huanwei.db" ) SQLALCHEMY_TRACK_MODIFICATIONS = FalseSECRET_KEY 用于 session 加密和 CSRF 保护,生产环境一定不能写死。SQLite 数据库文件我建议放在 instance 目录下,一是和源码分开,二是备份时只需要打包这一个文件,非常方便。
3. 数据表设计与 SQLAlchemy 建模:为人员、考勤、绩效打好地基
3.1 核心表结构设计思路
数据库是管理系统的地基,表结构设计错了后面要付出很大代价。我在这个项目里设计了四张核心表,加上一张用户表共五张:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| users | 管理员账号 | username, password_hash, role |
| workers | 工人档案 | worker_no, name, gender, phone, district, status |
| schedules | 排班记录 | worker_id, work_date, section, shift, remark |
| attendances | 考勤记录 | worker_id, att_date, status, remark |
| performances | 绩效记录 | worker_id, period, score, reason, operator_id |
设计时我纠结了一个点:考勤记录到底是每天每人一条,还是每天一条整体记录?最终我选了每天每人一条。因为环卫工人流动性大、路段分散,整体记录后期做筛选会很别扭;而按人按天记录,统计“某个人本月的出勤天数”或“某天哪些人缺勤”都非常直接。
3.2 models.py 模型代码
from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db = SQLAlchemy() class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) role = db.Column(db.String(20), default="admin") def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Worker(db.Model): __tablename__ = "workers" id = db.Column(db.Integer, primary_key=True) worker_no = db.Column(db.String(20), unique=True, nullable=False) name = db.Column(db.String(50), nullable=False) gender = db.Column(db.String(10)) phone = db.Column(db.String(20)) district = db.Column(db.String(100)) # 负责路段 status = db.Column(db.String(20), default="在职") created_at = db.Column(db.DateTime, default=datetime.now) class Schedule(db.Model): __tablename__ = "schedules" id = db.Column(db.Integer, primary_key=True) worker_id = db.Column(db.Integer, db.ForeignKey("workers.id")) work_date = db.Column(db.Date, nullable=False) section = db.Column(db.String(100)) shift = db.Column(db.String(20)) # 早班/中班/晚班 remark = db.Column(db.String(200)) worker = db.relationship("Worker", backref="schedules") class Attendance(db.Model): __tablename__ = "attendances" id = db.Column(db.Integer, primary_key=True) worker_id = db.Column(db.Integer, db.ForeignKey("workers.id")) att_date = db.Column(db.Date, nullable=False) status = db.Column(db.String(20), default="出勤") # 出勤/请假/缺勤 remark = db.Column(db.String(200)) worker = db.relationship("Worker", backref="attendances") class Performance(db.Model): __tablename__ = "performances" id = db.Column(db.Integer, primary_key=True) worker_id = db.Column(db.Integer, db.ForeignKey("workers.id")) period = db.Column(db.String(20)) # 月份,如 2025-03 score = db.Column(db.Float, nullable=False) reason = db.Column(db.String(200)) operator_id = db.Column(db.Integer, db.ForeignKey("users.id")) created_at = db.Column(db.DateTime, default=datetime.now) worker = db.relationship("Worker", backref="performances")这里有一个非常有用的设计:所有外键关系的另一端都用 relationship 做了反向引用。比如 Worker 模型里虽然没有直接写 schedules,但通过 backref,我可以非常自然地写出worker.schedules或者schedule.worker,这对模板渲染和业务统计帮助极大。
3.3 工人编号的生成策略
工人编号看起来是小事,但非常影响后续排班考勤的引用。我采用“年份 + 序列号”的方案,比如 2025 年入职的第一位工人,编号就是HW20250001。生成逻辑在添加工人时动态计算:
import datetime def gen_worker_no(): year = datetime.date.today().year last = Worker.query.filter( Worker.worker_no.like(f"HW{year}%") ).order_by(Worker.worker_no.desc()).first() if last: last_no = int(last.worker_no[-4:]) return f"HW{year}{last_no + 1:04d}" return f"HW{year}0001"这个方案的好处是:编号自带入职年份信息,而且直接与数据库索引兼容,检索非常快。不建议用数据库自增 id 当工人编号,因为一旦删除中间记录,编号就对不上了。
4. 后端业务接口实现:登录、增删改查与统计查询
4.1 登录鉴权与页面访问控制
管理系统的第一步就是登录鉴权。我用 Flask-Login 管理用户会话,在路由上通过@login_required装饰器控制访问权限:
from flask_login import LoginManager, login_user, logout_user, login_required, current_user login_manager = LoginManager() login_manager.login_view = "auth.login" @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id)) @bp.route("/login", methods=["GET", "POST"]) def login(): # 表单提交后校验用户名和密码 # 校验通过则 login_user(user) pass登录鉴权完成后,再给每个页面路由加@login_required。这样未登录用户访问任何管理页面都会自动跳转到登录页。这个机制是整个系统的安全基座,虽然简单,但必须在一开始就落地,不然后面每个新增页面都要手动处理权限,容易漏。
4.2 工人档案的增删改查路由
工人档案是一个典型的分页列表加新建/编辑页面组合。核心路由我拆成了四个:
from flask import render_template, redirect, url_for, request from models import db, Worker, gen_worker_no @bp.route("/workers") @login_required def list_workers(): page = request.args.get("page", 1, type=int) keyword = request.args.get("keyword", "").strip() query = Worker.query if keyword: query = query.filter(Worker.name.contains(keyword) | Worker.phone.contains(keyword)) pagination = query.paginate(page=page, per_page=10) return render_template("worker/list.html", pagination=pagination, keyword=keyword) @bp.route("/workers/add", methods=["GET", "POST"]) @login_required def add_worker(): # 表单校验通过后 worker_no = gen_worker_no() pass @bp.route("/workers/<int:wid>/edit", methods=["GET", "POST"]) @login_required def edit_worker(wid): worker = db.session.get(Worker, wid) # 更新后重定向回列表页 pass @bp.route("/workers/<int:wid>/delete", methods=["POST"]) @login_required def delete_worker(wid): worker = db.session.get(Worker, wid) # 删除前确认没有关联排班记录,避免外键报错 pass注意:删除操作一定要用 POST 而不是 GET。用 GET 接收删除请求,可能会被浏览器缓存或爬虫误触发,导致数据被意外清掉。
4.3 考勤和绩效的批量处理逻辑
考勤管理里最麻烦的不是单条登记,而是“批量生成某一天所有在职工人的考勤记录”。因为每天管理人员不可能一个个去点新增。我在设计时增加了一个当日考勤初始化函数:进入考勤页面时,先检查当天记录是否存在,如果不存在就自动为所有在职工人批量创建“出勤”记录。
def ensure_attendance_today(): today = datetime.date.today() existing = Attendance.query.filter_by(att_date=today).first() if existing: return workers = Worker.query.filter_by(status="在职").all() for w in workers: db.session.add(Attendance(worker_id=w.id, att_date=today, status="出勤")) db.session.commit()这个设计极大减少了管理员的重复操作。原本每天要花十分钟录考勤,现在只需要打开页面时系统自动生成,管理员只对异常情况做修改,比如把某个人改成“请假”。
绩效模块我采用了后置打分模式:月底管理员选择一个月份,系统先根据考勤数据自动算出一个基础分,再允许管理员手动调整分数并填写原因。基础分公式可以非常简单,比如出勤一天得 1 分,满勤额外加 5 分;主观分部分由管理员凭日常观察给出,并留备注。主观与客观数据分开存放的好处是:如果工人对分数有异议,可以清晰看到哪些来自考勤、哪些来自主观评价。
5. 前端页面与交互设计:让基层管理员真正用起来
5.1 模板继承与页面布局
后端功能再完整,页面不好用一样会失败。环卫管理系统的使用者普遍不是技术人员,页面必须做到“一眼就知道下一步点哪里”。我用 Jinja2 的模板继承搭了一个统一骨架:
<!-- base.html 骨架 --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}环卫工人管理系统{% endblock %}</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}"> </head> <body> {% if current_user.is_authenticated %} <nav class="top-nav"> <a href="{{ url_for('worker.list_workers') }}">工人档案</a> <a href="{{ url_for('schedule.list_schedule') }}">排班管理</a> <a href="{{ url_for('attendance.list_attendance') }}">考勤管理</a> <a href="{{ url_for('performance.list_performance') }}">绩效管理</a> <a href="{{ url_for('auth.logout') }}" class="nav-right">退出登录</a> </nav> {% endif %} <main class="container"> {% block content %}{% endblock %} </main> <script src="{{ url_for('static', filename='js/main.js') }}"></script> {% block scripts %}{% endblock %} </body> </html>页面里所有列表页都重复使用同一个表格样式,添加/编辑按钮统一放在表格右上角;操作按钮采用图标加文字,避免只放一个“铅笔”符号让年龄较大的管理员看不懂。这一点跟很多技术导向的项目不一样,但做业务系统就是这样,把用户的使用成本降到最低才是最核心的体验设计。
5.2 列表、搜索与分页的交互细节
列表页我采用“搜索框 + 筛选 + 分页 + 每页 10 条”的组合。这个组合虽然朴素,但对业务后台非常实用。搜索是实时执行的,用户输入姓名或手机号就能过滤,不用等整个列表加载完再翻页。
分页组件我直接使用 Flask-SQLAlchemy 的 Pagination 对象,在模板里渲染页码时,我做了一个细节处理:如果当前页与总页数相差超过 3 页,就只显示当前页附近的页码,避免页码过多视觉混乱。
5.3 用 ECharts 做简单的数据看板
数据看板我选择了 ECharts,通过 CDN 引入,不需要 npm、webpack 这些构建工具,对 Flask 项目非常友好。看板页放了四个基础图表:
- 折线图:近 30 天出勤率变化;
- 柱状图:各路段在职工人数量;
- 饼图:当前工人状态分布(在职/离职/休假);
- 表格:月度绩效排名前 10。
后端只需提供 JSON 接口,前端用 ECharts 的setOption渲染即可。比如出勤率数据的接口逻辑如下:
from sqlalchemy import func @bp.route("/api/attendance/stats") def attendance_stats(): end = datetime.date.today() start = end - datetime.timedelta(days=29) rows = db.session.query( Attendance.att_date, func.count(Attendance.id) ).filter( Attendance.att_date >= start, Attendance.att_date <= end, Attendance.status == "出勤" ).group_by(Attendance.att_date).all() return {"dates": [str(r[0]) for r in rows], "counts": [r[1] for r in rows]}这里必须提醒一个前端人容易忽略的点:统计查询要下推到数据库,不要在 Python 里一条条循环再累加。原因很简单:当数据量达到几千条时,循环方式会明显变慢,而 SQL 聚合查询几乎毫秒级返回。写管理系统早一点养成“能用聚合查询就不用循环”的习惯,后面处理大数据时受益很大。
6. 本地部署运行与踩坑复盘:从能跑到好用之间还有很多细节
6.1 初始化数据库与启动服务
项目第一次运行时,要先在 Python 交互环境里创建所有表并建立管理员账号:
pythonfrom app import app, db from models import User with app.app_context(): db.create_all() admin = User(username="admin") admin.set_password("123456") db.session.add(admin) db.session.commit()完成之后启动开发服务器:
python app.py # 默认运行在 http://127.0.0.1:5000本地访问 http://127.0.0.1:5000 就能看到登录页了。如果你想在局域网内让其他人访问,可以把 app.run 参数改为host="0.0.0.0", port=5000。需要注意的是,Flask 自带服务器只适合开发或极低并发内部使用,正式大规模上线建议用 Gunicorn + Nginx,这是另一篇文章的内容了,这里先不展开。
6.2 我在实际开发中踩过的三个坑
这里我整理三个本项目里真真切切踩过、也最值得分享的坑:
- SQLite 的日期比较问题。第一次写考勤统计时,我用 Python 侧的字符串与数据库日期字段比较,结果因为格式不一致,数据怎么查都不对。后来统一改用 datetime.date 对象和 SQLAlchemy 的
>=/<=比较,问题就消失了。因此所有日期参数在进入查询前一定要确认类型是 date 而不是字符串。 - 表单 CSRF 校验导致提交 400。加了 Flask-WTF 后,所有 POST 表单必须包含 CSRF 令牌。我一开始只写了
form.hidden_tag(),但模板里某些动态追加的行没有带令牌,结果批量提交时报 400。排查方法很直接:打开浏览器的开发者工具,看到“The CSRF token is missing”基本就能锁定问题。 - 虚拟环境路径被换导致启动报错。项目从一台机器拷到另一台机器时,venv 目录里的 Python 路径还是旧机器的绝对路径,直接运行会报找不到解释器。解决方法是打开 venv 下的 pyvenv.cfg 更新 home 路径,或者干脆删除旧 venv 重新创建。后面养成好习惯:项目迁移只拷贝源代码和依赖清单,不拷贝虚拟环境。
其实这套系统做到最后,“环卫工人管理系统”这个题目本身的技术含量并不高,真正有含金量的是那些从真实使用场景里逼出来的设计细节。一个字段要不要独立成表、一个操作该用 GET 还是 POST、一个统计数据该在哪里聚合,这些都是在跟使用者反复确认、在踩坑之后才慢慢积累起来的经验。把这些细节记下来,以后再做任何 Flask 管理类系统,你都会发现,流程已经可以像流水线一样顺畅地走完。