news 2026/10/1 4:27:37

Flask实战:环卫工人管理系统开发复盘——从数据库到前端

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask实战:环卫工人管理系统开发复盘——从数据库到前端

先直接说结论。这个项目就是用 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 大表。基于这句话,功能清单可以拆成:

  1. 登录注册,管理员身份验证;
  2. 工人档案的增删改查,支持姓名、路段、状态筛选;
  3. 排班管理,按路段分配工人,支持调整备注;
  4. 每日考勤登记,记录出勤、请假、缺勤;
  5. 绩效打分,按出勤和日常表现打分并备注原因;
  6. 数据统计页,展示出勤率、各路段人数分布等内容。

这里我要强调一点:功能清单不是写完就固定的。我在开发过程中跟使用者聊完以后,把“工资计算”从核心功能挪到了后续迭代里,因为工资涉及补贴、扣款等复杂规则,第一版强行做很容易做错。先保证核心流程跑通,再逐步加复杂规则,这种思路对中小型管理系统特别重要。

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 = False

SECRET_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 交互环境里创建所有表并建立管理员账号:

python
from 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 管理类系统,你都会发现,流程已经可以像流水线一样顺畅地走完。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:27:05

嵌入式GPU编程从入门到优化:并行计算、内存带宽与功耗控制实战

上周我帮一位朋友调试巡检机器人的视觉模块&#xff0c;CPU占用率跑到了90%多&#xff0c;图像还是掉帧。我把Sobel边缘检测和稠密光流两个算子挪到了板子自带的GPU上&#xff0c;延迟直接降到原来的三分之一&#xff0c;整机功耗还低了差不多两瓦。那次经历让我对嵌入式GPU编程…

作者头像 李华
网站建设 2026/10/1 4:26:20

手机远程操控 AI Agent:WebSocket 实时通信与多 Agent 统一管理实践

1. 手机远程操控 AI Agent 的整体设计思路1.1 为什么会有这个需求先说一个我自己的真实场景。我平时主力开发机是一台放在家里的工作站&#xff0c;跑着 Claude Code、Codex、OpenCode 这几个命令行 AI Agent&#xff0c;白天在公司用笔记本&#xff0c;晚上回家才碰得到那台机…

作者头像 李华
网站建设 2026/10/1 4:26:03

Linux ps命令详解:从PID到进程状态,彻底搞懂进程管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 4:25:44

MATLAB fmincon约束非线性优化实战:从算法选型到结果调试

写工程优化问题这几年&#xff0c;我被问得最多的就是"MATLAB里带约束的优化到底怎么搞"。很多人一上来就调fmincon&#xff0c;结果要么结果不对&#xff0c;要么直接报错&#xff0c;要么迭代半天不收敛。说实话&#xff0c;fmincon确实是MATLAB处理约束非线性优化…

作者头像 李华
网站建设 2026/10/1 4:24:21

读懂编程语言排行榜:从Python、Rust、TypeScript看技术趋势

每年11月的编程语言排行榜一出来&#xff0c;技术社区总要吵上几天&#xff1a;有人对着名次欢呼&#xff0c;有人吐槽“野榜”。入行这些年&#xff0c;我基本每个月都会刷一遍 TIOBE、PYPL、GitHub Octoverse、Stack Overflow 调查这些榜单&#xff0c;不是为了跟风吵架&…

作者头像 李华
网站建设 2026/10/1 4:24:16

二叉树最大深度:递归原理、栈溢出与迭代解法全解析

1. 拿到这道题先别急着递归&#xff0c;先聊聊它到底在考什么LeetCode Hot 100里面的题&#xff0c;说实话不是每一道都值得精刷&#xff0c;但"二叉树的最大深度"绝对值得。它排在第三十六题&#xff08;题号104&#xff09;&#xff0c;属于你看题目列表一眼扫过去…

作者头像 李华