news 2026/9/26 13:00:45

基于Flask的企业员工日程安排与考勤签到系统开发实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Flask的企业员工日程安排与考勤签到系统开发实践

1. 项目全景与需求拆解

1.1 这个系统到底解决什么问题

先聊点实在的。很多企业到现在还在用微信群接龙、Excel表格、纸质签到本来管理员工日程和考勤。你们别笑,我接过好几个真实项目,有的公司几十号人,每天早上靠行政在群里发消息问“今天谁请假谁外出”,下午再挨个统计签到情况,错漏百出。人一多信息一乱,月底对考勤账目的时候简直是灾难。

这个基于Flask的企业员工日程安排签到系统,说白了就是把两件事管起来:一是员工每天的工作日程安排和查看,二是上下班考勤打卡。再往大里说,这就是一个轻量级的办公自动化管理系统,通过一个网页端平台,让员工自己登录、自己登记日程、自己打卡签到,管理员在后台看全局数据。不需要装客户端,浏览器打开就能用。

我当初给一个二十人左右的小团队搭这套系统的时候,业务方提了几个硬性需求:第一,日程要能按日查看,每个人能写自己的计划;第二,签到要简单,不能搞人脸识别那种重方案,能用就行;第三,管理员要能看所有人的记录,月底能导出统计。听着简单,真做起来也有不少细节,尤其是日程和签到这两个模块撞在一起的时候,时间处理和状态判断就容易出岔子。

1.2 为什么用Flask而不是Django或者Spring Boot

技术上选择Flask,核心原因是轻。你要明白一个逻辑:企业内部的办公自动化系统,尤其是这种日程加签到的中小规模应用,并发量通常不会高到哪里去,几十个人同时在线已经是上限了。用重型框架Django,自带Admin后台、ORM、迁移工具,功能确实全,但杀鸡用牛刀,学习成本和部署体量都上去了。Spring Boot更不用提,Java那套环境配置、打包部署,在小团队内部落地的时候能把人折腾死。

Flask的优势在于它是一个微框架,核心只做路由和视图,其他能力通过扩展添加。SQLAlchemy管数据库,Jinja2管模板渲染,Flask-Login管会话登录,哪个需要就装哪个,结构非常清晰。对于这个系统来说,Flask足够轻量,启动快,部署简单,直接在服务器上跑一个Python进程就能提供服务。再配合SQLite做存储,连独立数据库都不用装,整个项目干净利落。

还有一个很实际的原因:Python的生态。pandas可以在月底统计考勤的时候直接读数据做聚合,openpyxl可以导出Excel报表,Requests可以对接企业微信通知。这些都是Flask项目里顺手就能接进来的能力。换成Java那套,实现同样功能要写的代码量翻倍都不止。

1.3 三种角色的权限模型设计

这个系统我设计了三类角色:普通员工、部门管理员、系统管理员。权限模型在项目初期就要想清楚,不然后面加功能的时候处处受制。

普通员工的操作范围是:登录后看到自己的日程列表和签到状态,可以新增、编辑、删除自己的日程,可以在有效时间窗口内完成签到打卡。

部门管理员的权限比普通员工多一块:可以查看本部门所有人员的日程安排和签到记录,方便协调工作,但不能改别人的日程。系统管理员则负责账号管理、全员数据查看、签到时间参数设置。

这个分级在设计数据库的时候就要体现在用户表上,用一个role字段区分,不要做成多个独立表。原因很简单:员工升职变成管理员,只需要改一个字段,不用搬数据。

2. 环境准备与项目初始化

2.1 Python环境与虚拟环境的坑

写Flask项目,第一步不是pip install flask,而是把Python环境理顺。我推荐直接用Python 3.10以上版本,别再守着3.6、3.7了,新特性用不上不说,很多依赖的新版本已经放弃老Python了。我之前踩过坑,系统里装了Python 3.6,结果SQLAlchemy 2.0直接装不上,被迫降级到1.4,白白浪费半天时间。

还有一个重点:虚拟环境。很多新手图省事,直接把包装到全局环境里。等项目多了就开始打架,这个项目要Flask 2.3,那个项目要Flask 2.0,pip install的时候互相覆盖,最后哪个项目都跑不起来。我通常每个项目单独建一个虚拟环境:

python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate

激活虚拟环境后,pip安装的包全部隔离在这个项目的目录里,跟全局环境互不干扰。这是Python项目开发的基本卫生习惯,真没必要省这一下。

2.2 依赖清单与版本选择

这个系统需要的依赖不多,核心就这几个:

  • Flask 2.3.x:Web框架,提供路由、请求处理、模板渲染
  • Flask-SQLAlchemy 3.0.x:数据库ORM,操作SQLite
  • Flask-Login:用户会话管理,处理登录状态
  • Werkzeug:Flask自带的依赖,用来做密码哈希
  • openpyxl:月底导出考勤Excel报表用

安装命令一句搞定:

pip install flask flask-sqlalchemy flask-login openpyxl

这里有个经验之谈:Flask-Login虽然好用,但它的会话管理逻辑有时候会跟自定义逻辑冲突。如果你发现登录状态老是不稳定,先检查SECRET_KEY是否设置,再检查用户加载回调是否写对。这两个地方是Flask-Login出错的高频区域。

2.3 项目目录结构规划

我习惯在项目一开始就把目录结构定好,避免后面代码越写越乱。这个系统的结构我这样组织:

office_system/ ├── app.py # 入口文件,创建应用实例 ├── models.py # 数据库模型定义 ├── extensions.py # 扩展初始化,避免循环引用 ├── config.py # 配置文件 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录注册相关路由 │ ├── schedule.py # 日程管理相关路由 │ └── attendance.py # 签到相关路由 ├── templates/ # Jinja2模板文件 │ ├── base.html │ ├── index.html │ ├── login.html │ ├── schedule.html │ └── attendance.html ├── static/ │ ├── css/ │ └── js/ └── venv/ # 虚拟环境

为什么不把所有路由都写在app.py里面?因为项目一旦超过三五个路由,单文件就会膨胀到几百行,改一个地方要翻半天。拆分成多个蓝图(Blueprint),每个模块自己管自己的路由,查找和维护都方便。我见过不少项目,功能不大但代码全堆在一个文件里,后面接手的人看着都头疼。代码的可维护性从来不是留给自己的,是留给三个月后的自己或者下一个同事的。

3. 数据库设计与核心模型

3.1 三张核心表的关系设计

这个系统的核心数据有三块:用户、日程、签到。对应的三张表以及它们之间的关系,是整个项目的地基,必须想清楚再动手。

用户表存储账号密码和角色信息。日程表记录员工的工作安排,通过user_id关联用户。签到表记录每天的打卡时间和状态,同样通过user_id关联用户。

为什么要单独建一张签到表?我之前遇到过一种偷懒设计,在用户表里加一个last_checkin_time字段,每天覆盖一次。问题是这只能记录最后一次签到时间,历史记录全丢了。月底统计的时候你拿什么数据来做考勤?所以签到记录必须是一条条存的,每天一条,可追溯可统计。

具体模型我用SQLAlchemy定义:

from extensions import db from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) role = db.Column(db.String(20), default='employee') # employee/manager/admin department = db.Column(db.String(50), default='') created_at = db.Column(db.DateTime, default=datetime.now) 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 Schedule(db.Model): __tablename__ = 'schedules' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) title = db.Column(db.String(120), nullable=False) content = db.Column(db.Text, default='') schedule_date = db.Column(db.Date, nullable=False) start_time = db.Column(db.Time, nullable=False) end_time = db.Column(db.Time, nullable=False) status = db.Column(db.String(20), default='pending') # pending/done/cancelled created_at = db.Column(db.DateTime, default=datetime.now) class Checkin(db.Model): __tablename__ = 'checkins' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) checkin_date = db.Column(db.Date, nullable=False) checkin_time = db.Column(db.DateTime, nullable=False) status = db.Column(db.String(20), default='normal') # normal/late/leave_early

3.2 数据库表设计时容易忽略的三个细节

第一,日期和时间用Date/Time类型,别全用DateTime。日程只需要关心某一天从几点到几点,日期跟时间分开存,查询的时候好统计。比如“查出今天所有日程”,直接用schedule_date == today就能过滤,不用做字符串截取。

第二,每个表都加created_at字段。很多人觉得同一个库里建表,谁知道哪个字段什么时候加进去的,等后面要排查数据问题的时候,这个时间戳能帮你快速定位是哪个时间段写入的脏数据。

第三,外键关系要显式声明并建立索引。我用user_id = db.Column(db.Integer, db.ForeignKey('users.id')),这样SQLAlchemy在联表查询的时候知道怎么关联。很多初学者为了省事直接存一个user_id字符串完事,等要join的时候发现类型对不上,或者外键约束写错,报错信息又是一堆火星文。

3.3 SQLite和MySQL怎么选

这个系统我默认用SQLite,文件型数据库,零配置文件,对中小规模办公系统完全够用。SQLite唯一要留意的是并发写的问题,多个用户同时写数据的时候可能会返回database is locked错误。Flask应用里有一个配置项可以缓解这个问题:

SQLALCHEMY_ENGINE_OPTIONS = { 'connect_args': {'timeout': 30} }

设置超时时间,让写操作在冲突时等待而不是立刻报错。如果团队规模超过百人,或者预计多台服务器并发访问,再考虑换MySQL不迟。到时候只需要改数据库URI和安装一个pymysql,SQLAlchemy的模型代码基本不用动。

4. 核心功能实现详解

4.1 登录注册模块与密码安全

登录注册是一套Web系统的门面,也是安全问题最容易出纰漏的地方。很多入门教程里的写法是:把密码明文存在数据库里。这在企业内部系统尤其危险,因为员工习惯用同一个密码在多个系统里,一旦数据库泄露,不只是这个系统遭殃。

正确做法是使用Werkzeug提供的哈希函数,生成密码哈希值存库。我在模型里已经写好了set_password和check_password两个方法,调用方式如下:

from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_login import login_user, logout_user, login_required, current_user from models import User from extensions import db auth_bp = Blueprint('auth', __name__) @auth_bp.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') existing = User.query.filter_by(username=username).first() if existing: flash('用户名已存在') return redirect(url_for('auth.register')) user = User(username=username) user.set_password(password) db.session.add(user) db.session.commit() flash('注册成功,请登录') return redirect(url_for('auth.login')) return render_template('register.html') @auth_bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = User.query.filter_by(username=username).first() if user and user.check_password(password): login_user(user) return redirect(url_for('index')) flash('用户名或密码错误') return render_template('login.html')

这里有几个细节值得说。一是用了flask-login的login_user,登录之后整个会话里都可以通过current_user拿到当前登录用户,不需要自己往session里塞user_id再手动取。二是密码校验成功后才调用login_user,失败不能登录。三是所有对外的路由都要记得加@login_required装饰器,控制访问权限。

4.2 日程管理的CRUD与时间冲突判断

日程管理的核心操作就是增删改查四个动作。员工登录后看到的是自己当天的日程列表,可以选择日期查看不同天的安排。

新增日程的时候,除了基本的标题和内容,还要做好时间段的校验。我之前看过一个案例,员工填的日程从下午三点到下午一点,结束时间比开始时间还早,这种数据进到库里,后面做统计的时候就是一颗定时炸弹。

因此在保存之前必须先做基础校验:

from datetime import datetime, time as dtime @schedule_bp.route('/schedule/add', methods=['POST']) @login_required def add_schedule(): title = request.form.get('title') content = request.form.get('content') schedule_date_str = request.form.get('schedule_date') start_str = request.form.get('start_time') end_str = request.form.get('end_time') if not title or not schedule_date_str or not start_str or not end_str: flash('请填写必填字段') return redirect(url_for('schedule.index')) schedule_date = datetime.strptime(schedule_date_str, '%Y-%m-%d').date() start_time = datetime.strptime(start_str, '%H:%M').time() end_time = datetime.strptime(end_str, '%H:%M').time() if end_time <= start_time: flash('结束时间必须晚于开始时间') return redirect(url_for('schedule.index')) schedule = Schedule( user_id=current_user.id, title=title, content=content, schedule_date=schedule_date, start_time=start_time, end_time=end_time ) db.session.add(schedule) db.session.commit() flash('日程添加成功') return redirect(url_for('schedule.index'))

这里的时间解析用datetime.strptime,一定要处理格式不匹配的异常。别以为前端日期控件传回来的格式就一定标准,你接手的项目里,什么牛鬼蛇神的数据格式都可能出现。

4.3 签到功能的业务规则设计

签到是这个系统里业务逻辑最微妙的部分。先说几点规则设计的关键思路。

第一,签到窗口。太早不算数,太晚按迟到处理。我设置的规则是:工作日早上8点前签到算正常,8点到9点签到算迟到,9点后签到算缺勤。这个时间参数放在config.py里集中管理,后期调整的话不用改业务代码。

CHECKIN_START_TIME = '07:00' CHECKIN_END_TIME = '09:30' CHECKIN_DEADLINE = '09:00'

第二,一天只能签到一次。这个限制必须在后端做,不能只靠前端按钮disable。实现方式是在写入前先查一下当天有没有记录:

@attendance_bp.route('/checkin', methods=['POST']) @login_required def do_checkin(): today = datetime.now().date() existing = Checkin.query.filter_by( user_id=current_user.id, checkin_date=today ).first() if existing: flash('今天已经签到过了') return redirect(url_for('attendance.index')) now = datetime.now() now_time = now.time() deadline = datetime.strptime(config.CHECKIN_DEADLINE, '%H:%M').time() status = 'normal' if now_time <= deadline else 'late' checkin = Checkin( user_id=current_user.id, checkin_date=today, checkin_time=now, status=status ) db.session.add(checkin) db.session.commit() flash(f'签到成功,状态:{"正常" if status == "normal" else "迟到"}') return redirect(url_for('attendance.index'))

第三,签到不是“点了就算成功”,而是“写进数据库才算成功”。前端可以做个动画反馈,但真正的状态必须以后端数据库记录为准。

4.4 前端页面与Jinja2模板渲染

Flask默认的模板引擎是Jinja2,写起来非常顺手。模板的核心逻辑是:视图函数把数据传给模板,模板通过继承和循环来渲染页面。

基础模板base.html定义页面骨架,具体的页面通过extends继承:

<!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/style.css') }}"> </head> <body> <nav> <div class="container"> <a href="{{ url_for('index') }}">首页</a> {% if current_user.is_authenticated %} <a href="{{ url_for('schedule.index') }}">日程管理</a> <a href="{{ url_for('attendance.index') }}">我的签到</a> {% if current_user.role != 'employee' %} <a href="{{ url_for('attendance.report') }}">考勤统计</a> {% endif %} <a href="{{ url_for('auth.logout') }}">退出登录</a> {% else %} <a href="{{ url_for('auth.login') }}">登录</a> <a href="{{ url_for('auth.register') }}">注册</a> {% endif %} </div> </nav> <main> {% with messages = get_flashed_messages() %} {% if messages %} <div class="flash-messages"> {% for message in messages %} <div class="flash-message">{{ message }}</div> {% endfor %} </div> {% endif %} {% endwith %} {% block content %}{% endblock %} </main> </body> </html>

日程页面循环渲染列表:

{% extends "base.html" %} {% block title %}我的日程{% endblock %} {% block content %} <h2>我的日程安排</h2> <form method="POST" action="{{ url_for('schedule.add_schedule') }}"> <input type="date" name="schedule_date" required> <input type="time" name="start_time" required> <input type="time" name="end_time" required> <input type="text" name="title" placeholder="日程标题" required> <textarea name="content" placeholder="日程内容"></textarea> <button type="submit">添加日程</button> </form> <table> <thead> <tr> <th>日期</th> <th>时间段</th> <th>标题</th> <th>内容</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> {% for item in schedules %} <tr> <td>{{ item.schedule_date }}</td> <td>{{ item.start_time.strftime('%H:%M') }} - {{ item.end_time.strftime('%H:%M') }}</td> <td>{{ item.title }}</td> <td>{{ item.content }}</td> <td>{{ item.status }}</td> <td> <a href="{{ url_for('schedule.complete', id=item.id) }}">完成</a> <a href="{{ url_for('schedule.delete_schedule', id=item.id) }}" onclick="return confirm('确定删除?')">删除</a> </td> </tr> {% endfor %} </tbody> </table> {% endblock %}

Jinja2模板可以用filter,比如strftime直接格式化时间对象,避免在视图函数里手动拼字符串。模板里还能做条件判断,权限不同显示不同操作按钮。

5. 完整运行流程梳理

5.1 从一个空文件夹到系统跑起来

整个项目从开发到本地运行,流程是这样的。

先把项目目录和环境建好,然后在extensions.py里初始化扩展:

from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = 'auth.login'

在app.py里创建应用实例并注册蓝图:

from flask import Flask from config import Config from extensions import db, login_manager from models import User def create_app(): app = Flask(__name__) app.config.from_object(Config) db.init_app(app) login_manager.init_app(app) from views.auth import auth_bp from views.schedule import schedule_bp from views.attendance import attendance_bp app.register_blueprint(auth_bp) app.register_blueprint(schedule_bp) app.register_blueprint(attendance_bp) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) with app.app_context(): db.create_all() return app app = create_app() if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)

关键点在于db.create_all()要在应用上下文中执行,不然SQLAlchemy找不到绑定的数据库实例,直接报RuntimeError。很多新手第一次跑项目就在这里卡住,报错信息又长又绕,其实就少了那一个with app.app_context()环境。

5.2 局域网部署与访问流程

开发调试用的app.run(debug=True)只能在本地跑,要让局域网内的其他员工也能访问,有两个地方要改。

第一,去掉debug模式。debug=True会开启调试器,别人能通过页面上的调试接口直接执行代码,这是生产环境的大忌。

第二,host参数设置为0.0.0.0,监听所有网络接口,这样同网段的电脑都能通过你的IP访问。启动后,终端会显示局域网地址,比如http://192.168.1.100:5000。

如果只是内部测试,这样已经够了。要是正式部署到服务器上,建议加一个gunicorn:

gunicorn -w 4 -b 0.0.0.0:5000 app:app

-w 4表示起4个工作进程,能够多进程并发处理请求。比app.run()自带的开发服务器靠谱得多,也扛得住更大的访问压力。

5.3 数据初始化和测试账号的坑

系统刚部署上去的时候,数据库里是空的,没有账号密码,大家都登录不进去。我的做法是写一个初始化脚本,命令行执行一下就能创建初始管理员账号:

# init_data.py from app import create_app from extensions import db from models import User app = create_app() with app.app_context(): admin = User.query.filter_by(username='admin').first() if not admin: admin = User(username='admin', role='admin', department='管理部') admin.set_password('admin123') db.session.add(admin) db.session.commit() print('管理员账号创建成功') else: print('管理员账号已存在')

以后要建普通的员工账号,可以直接在系统后台注册,也可以写一个批量导入脚本读Excel。小团队初期,注册功能已经够用了。

6. 常见问题与排查技巧实录

6.1 登录后跳转异常与找不到endpoint问题

Flask项目最常见的错误之一就是BuildError,提示找不到对应的endpoint。比如url_for('schedule.index')报错,但是你的蓝图中根本没有叫index的视图函数,或者路由定义的时候用了别的函数名。

排查思路很明确,把你蓝图里所有路由都列出来,逐个对一下url_for里的视图函数名。Blueprint名字和视图函数名是两个概念,url_for用的是“蓝图名.视图函数名”的形式。

还有一种情况是login_manager的login_view设置后,登录跳转循环出不来。检查一下未登录用户访问受保护页面时,Flask-Login是不是把他重定向到登录页,登录成功后又跳回原页面。这个逻辑本身是对的,但如果登录页自己也加了@login_required装饰器,就会造成无限重定向。

6.2 数据库锁定与新增字段不生效

SQLite的database is locked错误,多半是多个进程同时写库导致。前面提到了设置connect_args的timeout参数可以缓解,但更本质的解决办法是确认Flask应用是不是开了多个实例。

如果模型改了字段,但是db.create_all()不生效,那是因为create_all只负责创建不存在的表,不会去修改已存在的表结构。新增字段正确姿势是删掉旧表重新创建,或者用Flask-Migrate做迁移。开发初期数据不重要的情况下,我一般直接删库重建,省事:

rm -f instance/office.db python init.py

记住SQLite数据库文件默认放在instance目录下,如果你找不到数据库文件,去这个目录看看。

6.3 签到状态统计报表导出Excel

月底管理员需要导出考勤统计。我的做法是用openpyxl生成Excel文件,在内存中生成后通过Flask返回给浏览器下载。

from openpyxl import Workbook from openpyxl.utils import get_column_letter from flask import send_file import io @attendance_bp.route('/attendance/export') @login_required def export_attendance(): if current_user.role == 'employee': return '无权限', 403 month = request.args.get('month', datetime.now().strftime('%Y-%m')) wb = Workbook() ws = wb.active ws.title = '考勤记录' ws.append(['用户名', '日期', '签到时间', '状态']) start = datetime.strptime(month + '-01', '%Y-%m-%d').date() if start.month == 12: end = datetime(year=start.year+1, month=1, day=1).date() else: end = datetime(year=start.year, month=start.month+1, day=1).date() records = Checkin.query.filter( Checkin.checkin_date >= start, Checkin.checkin_date < end ).all() for record in records: user = User.query.get(record.user_id) ws.append([ user.username, record.checkin_date, record.checkin_time.strftime('%H:%M:%S'), record.status ]) bio = io.BytesIO() wb.save(bio) bio.seek(0) return send_file( bio, as_attachment=True, download_name=f'考勤统计_{month}.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' )

这个Excel导出的坑主要在openpyxl的下载文件名上:早期版本用的filename参数,新版本改成了download_name。如果下载后文件名是乱码或者没有后缀,检查一下Flask版本和send_file参数。另外数据量大的表,最好按月份过滤查询,别把全量数据一次性拉进来。

6.4 部署后样式丢失与图片路径问题

开发环境里一切正常,部署到服务器上CSS样式全丢了,这是Flask项目部署时经常遇到的静态文件问题。排查思路先看浏览器开发者工具的Network面板,CSS文件是不是404。

如果是404,检查模板里引用CSS的方式。用url_for('static', filename='css/style.css')生成路径,别写死成绝对路径。还有一个可能是Flask蓝图中静态文件夹的配置问题,或者在服务器反向代理配置中静态文件转发规则没写对。

对于部署到Windows服务器上的特别提醒:附件路径和上传路径要使用绝对路径,别用相对路径。我就遇到过项目部署到Windows服务器后,附件保存路径一直报错,后来发现是os.path.relpath在不同系统的路径分隔符处理上出的问题,改用os.path.abspath后一切正常。

6.5 时区与日期边界问题实战记录

签到系统里最容易出埋雷的就是日期边界问题。员工晚上11点打卡,存进数据库的时间是23:00:00,当天判断还正常。但如果有员工凌晨0点以后登录系统,看到的是新的一天,签到日期直接就跳到第二天了,这就跟企业的“自然日”概念冲突了。

我的处理方案是:所有日期操作都用本地时间,不依赖服务器时区。在Python里用datetime.now()获取的就是系统本地时间,只要部署的时候把服务器的时区设对,一般不会出大问题。

但如果系统要跨时区部署,建议把所有时间统一存成UTC,展示的时候再转换成本地时间。企业内部系统一般不涉及跨时区,用本地时间最直观,排查也方便。

7. 项目复盘与扩展建议

最近回看这套系统,有几个地方我觉得做得对,也有几个地方如果再给我一次机会一定会改掉。

做得对的地方是权限模型一开始就设计好了。后期加考勤统计、部门查看功能的时候,只需要判断current_user.role就行,完全没有重构用户体系。数据库表设计用了外键关联,联表查询很顺畅,没有出现“一张表存储所有业务”的糟糕设计。

可以改进的地方是密码策略太简单。目前只是做了哈希存储,没有强制密码复杂度,也没有密码过期策略。对于更注重安全的企业环境,可以考虑接入邮箱验证码、定期改密提醒。

另外一个扩展方向是消息通知。日程到期提醒、签到异常预警,这些如果接上企业微信或者邮件通知,实用性会提升一大截。我之前在另一个项目里用APScheduler做了定时任务,每天下午5点检查第二天有没有日程,有的话群发企业微信提醒。参考这个思路,你也可以给这个系统加一个通知模块。

最后一个建议,代码量控制。很多功能的逻辑在视图层就能解决,没必要过度设计引入Celery或者Redis这种重型组件。等业务量真正上来再升级不迟,中小型办公系统,保持简单直接就是最大的优点。

这个项目从设计到落地加起来大概花了一周多的业余时间,核心代码量并不大,但每一个环节都经过实际踩坑和验证。如果你正在搭类似的办公自动化系统,照着这个思路走一遍,很多弯路都可以提前绕开。

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

智慧交通数据治理:破解异构、时效、关联、质量四重困境

在智慧交通项目里摸爬滚打这几年&#xff0c;我最深的感触是&#xff1a;大家嘴上都在讲数据治理&#xff0c;实际动手时却经常被同一批问题反复绊倒。不管是卡口过车数据、浮动车GPS轨迹&#xff0c;还是信号灯控制日志、公交IC卡刷卡流水&#xff0c;单看任何一路数据似乎都挺…

作者头像 李华
网站建设 2026/9/26 12:59:16

VSCode 配置详解:离线版安装插件与 TaoToken 统一 Key 接入

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

作者头像 李华
网站建设 2026/9/26 12:58:19

Python入门第一步:环境搭建、基础语法与常见报错排查全攻略

第一次Python作业&#xff0c;看起来是编程入门里最简单的一步&#xff0c;但很多人恰恰就是被这一步劝退的。我见过不少同学课堂上听懂了、看示例也看懂了&#xff0c;可回家一打开电脑就是跑不通。最气人的是报错信息不告诉你错在哪&#xff0c;只甩出一屏英文&#xff0c;搞…

作者头像 李华
网站建设 2026/9/26 12:58:18

盲道障碍物识别实战:3500张图像分割数据集与U-Net训练避坑指南

简介&#xff1a;这是一套面向盲道识别与障碍物检测的多类别图像分割数据集&#xff0c;重点服务计算机视觉、智慧交通与辅助出行场景。数据准备阶段已完成标注与划分&#xff0c;训练集约两百三十张、验证集约八十张&#xff0c;全部采用图像目录与掩码目录组织&#xff0c;每…

作者头像 李华