news 2026/8/26 20:49:26

Python全栈实战:从零构建企业级考勤系统(Flask+SQLite+Vue)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python全栈实战:从零构建企业级考勤系统(Flask+SQLite+Vue)

1. 项目概述:为什么用Python做考勤系统?

如果你在一家中小型公司负责行政或IT,或者你是一个想用技术解决实际问题的开发者,大概率都遇到过考勤管理的麻烦。纸质签到容易丢失、代签,Excel表格统计起来费时费力,还容易出错。市面上的专业考勤系统功能强大,但要么价格昂贵,要么流程僵化,很多小团队用起来并不顺手。

这时候,自己动手用Python写一个电子考勤系统,就成了一个非常务实的选择。Python这门语言,以其清晰的语法和丰富的库生态,特别适合快速开发这类数据处理密集型的应用。它不像Java那样需要厚重的框架,也不像C++那样需要关注底层细节,你可以把精力集中在业务逻辑上——如何记录、如何计算、如何呈现。

这个项目的核心价值,远不止是“学会用Python操作数据库和Excel”。它是一次完整的全栈开发实战演练。你会涉及到用户交互(前端界面或命令行)、数据持久化(数据库)、业务逻辑(考勤规则计算)和结果输出(报表生成)。通过它,你能把散落的知识点,比如文件操作、时间处理、数据分析和简单的Web开发,串成一个解决真实问题的完整方案。对于初学者,这是迈向“能写有用程序”的关键一步;对于有经验的开发者,这是一个优化内部流程、展示自动化价值的绝佳案例。

接下来,我将以一个面向中小团队、基于本地化运行的考勤系统为例,拆解从设计到实现的完整过程。我们会采用前后端分离的思路,后端用Flask快速搭建API,前端用简单的HTML+JavaScript,数据库用轻量级的SQLite,最终能实现员工打卡、请假申请、月度统计和报表导出等功能。

2. 系统核心设计与技术选型

在动手写代码之前,花点时间想清楚系统要做什么、怎么做,能避免后期大量的返工。一个基础的考勤系统,核心模块通常包括:员工信息管理、每日考勤记录、请假/加班等特殊考勤事件管理,以及统计报表。

2.1 架构设计:轻量级C/S或B/S?

对于内部使用的工具,我首推基于浏览器的B/S(浏览器/服务器)架构。理由很简单:零客户端安装。员工在任何一台能连内网的电脑上,打开浏览器就能打卡,管理员也能随时随地查看报表。维护也只需要更新服务器端的代码。

我们将采用前后端分离的微服务思想,但为了简化,把前后端项目放在一起管理:

  • 后端(API Server):使用Flask框架。它足够轻量、灵活,学习曲线平缓,非常适合快速构建RESTful API。相比Django的“大而全”,Flask的“微”让我们可以从零开始,清晰地定义每一个数据接口。
  • 前端(Web界面):使用原生HTML、CSS 和 JavaScript,搭配一点jQueryVue.js来简化DOM操作。对于这种内部工具,不必引入React或Angular等重型框架,保持简单可维护是关键。我们将用前端页面调用后端API,实现数据交互。
  • 数据库:选择SQLite。它是一个文件型数据库,无需安装和配置独立的数据库服务,程序直接读写一个.db文件。这对于单机或小规模并发应用来说,部署简单,性能也完全足够。当未来需要扩展时,可以相对平滑地迁移到MySQL或PostgreSQL。
  • 报表生成:使用openpyxl库操作Excel文件。这是Python处理Excel事实上的标准库之一,可以完美地创建、编辑.xlsx文件,用于生成格式规范的考勤统计表。

注意:虽然SQLite很方便,但它对高并发写的支持较弱。如果预估同时在线打卡人数会超过几十人,建议在项目初期就直接使用MySQL,避免后期数据迁移的麻烦。这里为了演示和轻量部署,我们选择SQLite。

2.2 数据库表结构设计

数据库设计是系统的基石。设计良好的表结构,能让后续的编码和查询逻辑变得清晰。我们至少需要四张核心表:

1. 员工表 (employees)这张表存储员工的基本信息。

CREATE TABLE employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键,自增 employee_id VARCHAR(20) UNIQUE NOT NULL, -- 工号,唯一 name VARCHAR(50) NOT NULL, -- 姓名 department VARCHAR(50), -- 部门 hire_date DATE, -- 入职日期 is_active BOOLEAN DEFAULT 1 -- 是否在职 );

设计思路employee_id(工号)设为主键或唯一键,比用姓名更准确。is_active字段用于软删除,标记离职员工,而不是物理删除记录,便于历史数据追溯。

2. 考勤记录表 (attendance_records)这是最核心的表,记录每一次打卡行为。

CREATE TABLE attendance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id VARCHAR(20) NOT NULL, -- 关联员工工号 check_time DATETIME NOT NULL, -- 打卡时间 check_type VARCHAR(10) NOT NULL, -- 'IN' 上班 / 'OUT' 下班 source VARCHAR(20) DEFAULT 'WEB', -- 打卡来源,如 'WEB', 'MOBILE' ip_address VARCHAR(45), -- 打卡IP,用于简单安全校验 FOREIGN KEY (employee_id) REFERENCES employees(employee_id) );

设计思路:将上班、下班打卡都记录在同一张表,用check_type区分。记录check_time的精确到秒的时间戳。sourceip_address字段可用于分析打卡渠道和进行简单的防作弊校验(例如,限制同一IP短时间内多次打卡)。

3. 请假记录表 (leave_records)

CREATE TABLE leave_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id VARCHAR(20) NOT NULL, leave_type VARCHAR(20) NOT NULL, -- 请假类型:年假、病假、事假等 start_date DATE NOT NULL, end_date DATE NOT NULL, days FLOAT NOT NULL, -- 请假天数 status VARCHAR(20) DEFAULT 'PENDING', -- 状态:PENDING, APPROVED, REJECTED reason TEXT, -- 请假事由 FOREIGN KEY (employee_id) REFERENCES employees(employee_id) );

设计思路days字段存储浮点数,方便处理半天假(0.5天)。status字段实现简单的审批流程。

4. 工作日历表 (work_calendar)这是很多人会忽略但极其重要的一张表,用于定义法定工作日、休息日和公司特殊安排。

CREATE TABLE work_calendar ( date DATE PRIMARY KEY, -- 日期 is_workday BOOLEAN NOT NULL, -- 是否为工作日 description VARCHAR(100) -- 描述,如“国庆节调休”、“公司年会” );

设计思路:预先导入国家法定节假日和调休安排。在计算迟到、早退、加班时,必须依据此表来判断当天是否是正常工作日,而不是简单粗暴地看是否是周末。

3. 后端API开发实战(Flask + SQLite)

有了清晰的设计,我们就可以开始搭建后端服务了。我们将使用Flask来创建提供数据增删改查的API接口。

3.1 项目初始化与依赖安装

首先,创建一个新的项目目录,并建立虚拟环境,这是保持项目依赖隔离的好习惯。

mkdir attendance_system cd attendance_system python -m venv venv # 创建虚拟环境 # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装核心依赖 pip install flask flask-cors openpyxl

flask-cors库用于处理前端跨域请求,在开发阶段非常有用。

项目目录结构可以这样组织:

attendance_system/ ├── app.py # Flask应用主入口 ├── config.py # 配置文件 ├── models.py # 数据库模型定义 ├── routes/ # 路由蓝图 │ ├── __init__.py │ ├── employee.py │ ├── attendance.py │ └── report.py ├── utils/ # 工具函数 │ └── database.py ├── static/ # 静态文件(前端页面) │ ├── index.html │ ├── css/ │ └── js/ └── instance/ # 实例文件夹,存放数据库文件 └── attendance.db

3.2 数据库连接与模型定义

models.py中,我们不直接使用复杂的ORM(如SQLAlchemy),为了更直观地理解SQL,我们先使用Python内置的sqlite3库进行封装。

# utils/database.py import sqlite3 import os from flask import g DATABASE = os.path.join(os.getcwd(), 'instance', 'attendance.db') def get_db(): """获取数据库连接,使用Flask的g对象确保线程安全""" if 'db' not in g: g.db = sqlite3.connect(DATABASE) g.db.row_factory = sqlite3.Row # 使返回结果像字典一样可访问 return g.db def close_db(e=None): """关闭数据库连接""" db = g.pop('db', None) if db is not None: db.close() def init_db(): """初始化数据库,创建所有表""" db = get_db() with open('schema.sql', 'r') as f: db.executescript(f.read()) db.commit()

然后,将之前设计的SQL表语句,保存到一个schema.sql文件中。在app.py的工厂函数里,我们可以提供一个初始化数据库的命令。

3.3 核心API接口实现

我们以“打卡”和“查询考勤”这两个最核心的功能为例,展示API的实现。

1. 打卡接口 (POST /api/attendance/check)这个接口接收员工工号和打卡类型,记录打卡时间。

# routes/attendance.py from flask import Blueprint, request, jsonify, g from datetime import datetime import pytz # 需要安装 pip install pytz bp = Blueprint('attendance', __name__, url_prefix='/api/attendance') @bp.route('/check', methods=['POST']) def check_in_out(): data = request.get_json() employee_id = data.get('employee_id') check_type = data.get('check_type', 'IN').upper() # 默认为上班打卡 if not employee_id or check_type not in ['IN', 'OUT']: return jsonify({'error': '参数错误'}), 400 # 验证员工是否存在且在职 db = g.db cursor = db.execute('SELECT is_active FROM employees WHERE employee_id = ?', (employee_id,)) employee = cursor.fetchone() if not employee or not employee['is_active']: return jsonify({'error': '员工不存在或已离职'}), 400 # 获取当前时间(建议使用UTC时间存储,或明确的时区) local_tz = pytz.timezone('Asia/Shanghai') now_utc = datetime.utcnow() now_local = pytz.utc.localize(now_utc).astimezone(local_tz) # 防止短时间内重复打卡(例如60秒内) cursor = db.execute(''' SELECT check_type FROM attendance_records WHERE employee_id = ? AND date(check_time) = date(?) AND check_type = ? AND julianday(?) - julianday(check_time) < 60/86400.0 ''', (employee_id, now_local, check_type, now_local)) if cursor.fetchone(): return jsonify({'error': '请不要重复打卡'}), 400 # 插入打卡记录 db.execute(''' INSERT INTO attendance_records (employee_id, check_time, check_type, ip_address) VALUES (?, ?, ?, ?) ''', (employee_id, now_local, check_type, request.remote_addr)) db.commit() return jsonify({ 'message': f'{check_type}打卡成功', 'employee_id': employee_id, 'check_time': now_local.strftime('%Y-%m-%d %H:%M:%S') }), 200

实操心得:时间处理是考勤系统的坑点之一。强烈建议在数据库中统一使用UTC时间存储,并在业务逻辑中根据需要进行转换。同时,记录客户端IP (request.remote_addr) 可以作为一项简单的安全审计日志。上面的重复打卡检查是一个基础策略,实际中可能需要更复杂的规则,比如同一天同一类型只允许打一次卡。

2. 查询个人考勤接口 (GET /api/attendance/<employee_id>?month=2023-10)这个接口用于查询某个员工指定月份的每日考勤情况。

@bp.route('/<employee_id>', methods=['GET']) def get_attendance(employee_id): month = request.args.get('month') # 格式:2023-10 if not month: return jsonify({'error': '请指定月份参数,如 ?month=2023-10'}), 400 db = g.db # 查询该员工指定月份的所有打卡记录,按天分组 cursor = db.execute(''' SELECT date(check_time) as work_date, GROUP_CONCAT(time(check_time) || '|' || check_type) as checks FROM attendance_records WHERE employee_id = ? AND strftime('%Y-%m', check_time) = ? GROUP BY date(check_time) ORDER BY work_date ''', (employee_id, month)) records = cursor.fetchall() # 格式化返回数据 result = [] for row in records: checks = [] if row['checks']: for c in row['checks'].split(','): time_str, type_str = c.split('|') checks.append({'time': time_str, 'type': type_str}) result.append({ 'date': row['work_date'], 'checks': checks }) return jsonify({'employee_id': employee_id, 'month': month, 'attendance': result}), 200

这个查询使用了SQLite的strftime函数进行日期格式化,并用GROUP_CONCAT将一天内的多次打卡记录聚合到一个字段中,方便前端解析。这是一种在数据库层进行初步数据加工的常用技巧,可以减少应用层的处理逻辑。

4. 前端界面与交互实现

后端API准备好了,我们需要一个让用户能操作的界面。这里我们构建一个极其简单的管理后台首页,包含打卡和月度考勤查看功能。

4.1 基础HTML结构与打卡功能

创建一个static/index.html文件。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>简易考勤系统</title> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <style> body { font-family: sans-serif; margin: 20px; } .container { max-width: 800px; margin: auto; } .card { border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; border-radius: 5px; } button { padding: 10px 15px; margin: 5px; cursor: pointer; } .success { color: green; } .error { color: red; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: center; } </style> </head> <body> <div id="app" class="container"> <h1>员工考勤系统</h1> <div class="card"> <h3>每日打卡</h3> <div> <label>工号:</label> <input v-model="employeeId" placeholder="请输入你的工号"> </div> <div style="margin-top: 10px;"> <button @click="check('IN')" :disabled="!employeeId">上班打卡</button> <button @click="check('OUT')" :disabled="!employeeId">下班打卡</button> </div> <p :class="messageClass">{{ checkMessage }}</p> </div> <div class="card"> <h3>月度考勤查询</h3> <div> <label>工号:</label> <input v-model="queryEmployeeId" placeholder="请输入要查询的工号"> <label>月份:</label> <input type="month" v-model="queryMonth"> <button @click="fetchAttendance" :disabled="!queryEmployeeId || !queryMonth">查询</button> </div> <table v-if="attendanceData.length > 0"> <thead> <tr> <th>日期</th> <th>上班时间</th> <th>下班时间</th> <th>状态</th> </tr> </thead> <tbody> <tr v-for="item in attendanceData" :key="item.date"> <td>{{ item.date }}</td> <td>{{ item.check_in || '--' }}</td> <td>{{ item.check_out || '--' }}</td> <td>{{ item.status }}</td> </tr> </tbody> </table> <p v-else>暂无考勤数据或未查询。</p> </div> </div> <script> new Vue({ el: '#app', data: { employeeId: '', queryEmployeeId: '', queryMonth: new Date().toISOString().slice(0,7), // 默认当前年月 checkMessage: '', messageClass: '', attendanceData: [] }, methods: { check(type) { axios.post('/api/attendance/check', { employee_id: this.employeeId, check_type: type }).then(response => { this.checkMessage = `打卡成功!时间:${response.data.check_time}`; this.messageClass = 'success'; }).catch(error => { this.checkMessage = `打卡失败:${error.response?.data?.error || '网络错误'}`; this.messageClass = 'error'; }); }, fetchAttendance() { axios.get(`/api/attendance/${this.queryEmployeeId}?month=${this.queryMonth}`) .then(response => { // 将后端返回的checks数组,转换为前端表格需要的格式 this.attendanceData = response.data.attendance.map(day => { let check_in = '', check_out = ''; day.checks.forEach(c => { if (c.type === 'IN') check_in = c.time.substr(0,5); // 只显示时分 if (c.type === 'OUT') check_out = c.time.substr(0,5); }); // 简单的状态判断(此处逻辑可复杂化) let status = '正常'; if (!check_in) status = '缺卡'; else if (check_in > '09:00') status = '迟到'; return { date: day.date, check_in: check_in, check_out: check_out, status: status }; }); }).catch(error => { alert(`查询失败:${error.response?.data?.error || '未知错误'}`); this.attendanceData = []; }); } } }); </script> </body> </html>

这个前端页面使用Vue.js进行数据绑定和事件处理,用Axios库调用后端API。界面分为两大块:打卡区和查询区。打卡区实现了上班/下班打卡功能;查询区可以根据工号和月份,查询并表格化展示每日的上下班时间及一个简单的考勤状态。

4.2 与后端联调的关键点

为了让前端能访问到后端API,我们需要在Flask应用中配置静态文件服务和CORS。

app.py中:

from flask import Flask, send_from_directory from flask_cors import CORS def create_app(): app = Flask(__name__, static_folder='static') CORS(app) # 允许跨域,开发时使用。生产环境应配置具体的源。 # ... 注册蓝图等其它配置 ... @app.route('/') def index(): # 直接返回前端页面 return send_from_directory(app.static_folder, 'index.html') return app if __name__ == '__main__': app = create_app() app.run(debug=True, host='0.0.0.0', port=5000)

运行python app.py,访问http://localhost:5000就能看到界面了。

注意事项:上面的CORS配置 (CORS(app)) 在开发时很方便,但在生产环境是极不安全的,因为它允许任何网站向你的API发起请求。生产环境中,应该使用CORS(app, resources={r"/api/*": {"origins": "https://your-domain.com"}})来限制源。

5. 考勤统计与报表导出

打卡数据有了,下一步就是生成管理人员需要的统计报表。这是体现系统价值的关键环节。

5.1 核心考勤统计逻辑

统计的核心是遍历一个月的每一天,结合打卡记录、请假记录和工作日历,判断员工当天的出勤状态。我们编写一个统计函数,它接受员工ID和月份,返回该月的每日明细和汇总。

# utils/attendance_calculator.py from datetime import datetime, timedelta import calendar def calculate_monthly_attendance(db, employee_id, year_month): """计算指定员工指定月份的考勤情况""" year, month = map(int, year_month.split('-')) # 获取该月第一天和最后一天 _, last_day = calendar.monthrange(year, month) start_date = datetime(year, month, 1).date() end_date = datetime(year, month, last_day).date() # 1. 获取该员工所有打卡记录(按日期分组) cursor = db.execute(''' SELECT date(check_time) as d, check_type, time(check_time) as t FROM attendance_records WHERE employee_id = ? AND date(check_time) BETWEEN ? AND ? ORDER BY check_time ''', (employee_id, start_date, end_date)) raw_records = cursor.fetchall() # 转换为字典:{日期: [{'type':'IN', 'time':'09:00'}, ...]} daily_checks = {} for r in raw_records: daily_checks.setdefault(r['d'], []).append({'type': r['check_type'], 'time': r['t']}) # 2. 获取该员工请假记录 cursor = db.execute(''' SELECT start_date, end_date, days, leave_type, status FROM leave_records WHERE employee_id = ? AND status = 'APPROVED' AND NOT (end_date < ? OR start_date > ?) ''', (employee_id, start_date, end_date)) leave_records = cursor.fetchall() # 3. 获取工作日历 cursor = db.execute('SELECT date, is_workday FROM work_calendar WHERE date BETWEEN ? AND ?', (start_date, end_date)) calendar_dict = {row['date']: row['is_workday'] for row in cursor.fetchall()} results = [] total_workdays = 0 total_actual_days = 0 total_late_minutes = 0 current_date = start_date while current_date <= end_date: # 判断是否为工作日(先查特殊日历,再判断周末) is_workday = calendar_dict.get(current_date) if is_workday is None: # 如果日历表中未定义,则按常规周末判断(周一为0,周日为6) is_workday = current_date.weekday() < 5 day_result = { 'date': current_date.isoformat(), 'is_workday': is_workday, 'check_in': None, 'check_out': None, 'status': '未打卡', 'leave_type': None } if is_workday: total_workdays += 1 # 处理请假 on_leave = False for leave in leave_records: if leave['start_date'] <= current_date <= leave['end_date']: day_result['leave_type'] = leave['leave_type'] on_leave = True break # 处理打卡 checks = daily_checks.get(current_date, []) if checks: in_times = [c['time'] for c in checks if c['type'] == 'IN'] out_times = [c['time'] for c in checks if c['type'] == 'OUT'] day_result['check_in'] = min(in_times) if in_times else None day_result['check_out'] = max(out_times) if out_times else None # 状态判定(简化逻辑) if on_leave: day_result['status'] = f"请假({day_result['leave_type']})" elif not is_workday: day_result['status'] = '休息日' else: # 工作日,有打卡记录 total_actual_days += 1 if not day_result['check_in']: day_result['status'] = '缺卡' else: # 判断是否迟到(假设9:00上班) from datetime import datetime as dt check_in_dt = dt.strptime(day_result['check_in'], '%H:%M:%S') if check_in_dt.time() > dt.strptime('09:00:00', '%H:%M:%S').time(): late_seconds = (check_in_dt - dt.strptime('09:00:00', '%H:%M:%S')).seconds late_minutes = late_seconds // 60 total_late_minutes += late_minutes day_result['status'] = f'迟到{late_minutes}分钟' else: day_result['status'] = '正常' elif on_leave: day_result['status'] = f"请假({day_result['leave_type']})" elif not is_workday: day_result['status'] = '休息日' # 否则保持‘未打卡’ results.append(day_result) current_date += timedelta(days=1) summary = { 'employee_id': employee_id, 'year_month': year_month, 'total_workdays': total_workdays, 'total_actual_days': total_actual_days, 'attendance_rate': round((total_actual_days / total_workdays * 100), 2) if total_workdays > 0 else 0, 'total_late_minutes': total_late_minutes } return {'daily_details': results, 'summary': summary}

这个函数是考勤系统的“大脑”,逻辑相对复杂。它依次处理了:1. 基础打卡数据聚合;2. 请假记录的覆盖判断;3. 工作日历的优先级应用;4. 最终状态的判定(正常、迟到、缺卡、请假、休息日)。在实际项目中,这里的判定规则(如迟到阈值、是否考虑弹性工时、加班判定等)需要根据公司制度详细定义。

5.2 使用openpyxl生成Excel报表

统计好的数据,最终需要以报表形式交付。我们使用openpyxl库生成一个格式清晰的Excel文件。

# routes/report.py from flask import Blueprint, send_file, request, jsonify, g from openpyxl import Workbook from openpyxl.styles import Font, Alignment, Border, Side, PatternFill from io import BytesIO from utils.attendance_calculator import calculate_monthly_attendance bp = Blueprint('report', __name__, url_prefix='/api/report') @bp.route('/monthly/<employee_id>', methods=['GET']) def generate_monthly_report(employee_id): month = request.args.get('month') if not month: return jsonify({'error': '缺少月份参数'}), 400 # 1. 计算考勤数据 data = calculate_monthly_attendance(g.db, employee_id, month) daily_details = data['daily_details'] summary = data['summary'] # 2. 创建Excel工作簿 wb = Workbook() ws = wb.active ws.title = f"{month}考勤明细" # 3. 设置标题和表头 title_cell = ws['A1'] title_cell.value = f"员工 {employee_id} {month} 月考勤统计表" title_cell.font = Font(size=14, bold=True) ws.merge_cells('A1:F1') title_cell.alignment = Alignment(horizontal='center') headers = ['日期', '是否工作日', '上班时间', '下班时间', '请假类型', '考勤状态'] for col, header in enumerate(headers, start=1): cell = ws.cell(row=3, column=col, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center') cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid") # 设置边框 thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) cell.border = thin_border # 4. 填充每日数据 for i, day in enumerate(daily_details, start=1): row = 3 + i ws.cell(row=row, column=1, value=day['date']) ws.cell(row=row, column=2, value='是' if day['is_workday'] else '否') ws.cell(row=row, column=3, value=day['check_in'] or '') ws.cell(row=row, column=4, value=day['check_out'] or '') ws.cell(row=row, column=5, value=day['leave_type'] or '') status_cell = ws.cell(row=row, column=6, value=day['status']) # 根据状态标记颜色 if '迟到' in day['status']: status_cell.fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") # 黄色 elif '缺卡' in day['status'] or '未打卡' in day['status']: status_cell.fill = PatternFill(start_color="FF9999", end_color="FF9999", fill_type="solid") # 红色 # 5. 添加汇总行 summary_row = len(daily_details) + 5 ws.cell(row=summary_row, column=1, value='汇总信息').font = Font(bold=True) ws.cell(row=summary_row+1, column=1, value='应出勤工作日:') ws.cell(row=summary_row+1, column=2, value=summary['total_workdays']) ws.cell(row=summary_row+2, column=1, value='实际出勤天数:') ws.cell(row=summary_row+2, column=2, value=summary['total_actual_days']) ws.cell(row=summary_row+3, column=1, value='出勤率:') ws.cell(row=summary_row+3, column=2, value=f"{summary['attendance_rate']}%") ws.cell(row=summary_row+4, column=1, value='累计迟到分钟数:') ws.cell(row=summary_row+4, column=2, value=summary['total_late_minutes']) # 6. 调整列宽 for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = (max_length + 2) ws.column_dimensions[column_letter].width = adjusted_width # 7. 将工作簿保存到内存字节流,并返回给前端 virtual_workbook = BytesIO() wb.save(virtual_workbook) virtual_workbook.seek(0) return send_file(virtual_workbook, download_name=f'attendance_report_{employee_id}_{month}.xlsx', as_attachment=True, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')

这个API接口会生成一个包含明细和汇总的Excel文件,并直接触发浏览器下载。openpyxl库提供了丰富的样式控制能力,可以让报表看起来更专业。通过BytesIO在内存中生成文件,避免了在服务器磁盘上创建临时文件的麻烦。

6. 部署、优化与安全考量

一个能跑起来的系统,和一个能稳定用于生产环境的系统,中间还有不少距离。以下是几个关键的提升点。

6.1 基础部署与运行

最简单的部署方式,就是在服务器上像开发时一样直接运行Flask开发服务器。但这绝对不适用于生产环境,因为它的性能、稳定性和安全性都很差。

推荐的生产部署方案

  1. WSGI Server:使用GunicornuWSGI作为应用服务器。例如,用Gunicorn启动:gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"-w 4表示启动4个worker进程处理并发请求。
  2. 反向代理:在前端使用NginxApache。它们处理静态文件(如你的HTML/CSS/JS)效率更高,并且可以作为反向代理,将动态请求转发给Gunicorn,同时还能配置SSL、负载均衡等。
  3. 进程管理:使用systemdSupervisor来管理Gunicorn进程,确保应用在崩溃后能自动重启,并在服务器开机时自动启动。

一个简单的Nginx配置示例 (/etc/nginx/sites-available/attendance):

server { listen 80; server_name your-domain.com; # 或服务器IP location / { proxy_pass http://127.0.0.1:8000; # 转发给Gunicorn proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 可选:让Nginx直接处理静态文件,减轻Flask负担 location /static { alias /path/to/your/attendance_system/static; expires 30d; } }

6.2 性能与数据安全优化

  • 数据库连接池:在Web应用中,频繁创建和关闭数据库连接开销很大。可以使用DBUtilsSQLAlchemy(它内置连接池)来管理数据库连接。
  • 关键操作日志:所有打卡、请假审批、管理员操作都应记录日志(操作人、时间、IP、具体动作),便于审计和排查问题。
  • 密码与权限:目前的示例没有用户登录。在实际系统中,必须引入用户认证。可以使用Flask-Login管理会话,密码务必使用Werkzeuggenerate_password_hashcheck_password_hash进行加盐哈希存储,绝对禁止明文存储密码
  • 参数化查询:我们一直使用的?占位符就是参数化查询,这是防止SQL注入攻击最基本也是最有效的手段,务必在所有数据库操作中坚持使用。
  • API限流:对于打卡等接口,可以增加限流(例如,同一IP每分钟最多请求10次),防止恶意刷接口。可以使用Flask-Limiter扩展轻松实现。

6.3 常见问题排查与调试技巧

  1. 时间始终不对:这是最常见的问题。确保服务器、数据库和代码中使用统一的时区。最佳实践是:在数据库中存储UTC时间,在业务逻辑中根据用户所在时区进行转换。可以使用pytz或 Python 3.9+ 的zoneinfo模块。
  2. 打卡记录重复或丢失:检查前端是否因网络问题重复提交;在后端接口中加入更严格的幂等性检查,例如基于“员工ID+日期+打卡类型”生成一个唯一键进行判断。
  3. 月末统计计算错误:重点检查“工作日历”表的数据是否准确覆盖了法定节假日和调休日。统计逻辑的边界条件(如跨午夜打卡、请假半天)要反复测试。
  4. 前端页面无法访问API:检查Flask后端是否运行,以及前端Axios请求的URL是否正确。浏览器按F12打开开发者工具,在“网络(Network)”标签页查看请求是否发出、响应状态码和错误信息。
  5. SQLite数据库被锁:当多个进程/线程同时写入SQLite时,可能会遇到“database is locked”错误。对于写入不频繁的小型应用,可以尝试设置timeout参数(sqlite3.connect(DATABASE, timeout=10))。如果并发写入需求高,必须升级到MySQL或PostgreSQL。

开发过程中,善用Flask的Debug模式,并配合打印日志(printlogging模块),能快速定位问题。对于复杂的考勤计算逻辑,建议编写单元测试,用不同的打卡、请假数据组合来验证统计结果的正确性。

这个用Python搭建的电子考勤系统,从一个具体的需求出发,串联起了Web开发、数据库设计、API构建、前端交互和数据处理等多个核心技能点。它可能不像商业软件那样功能炫酷,但完全契合特定团队的需求,并且拥有无限的定制可能性。你可以在此基础上,轻松地加入排班管理、加班申请、薪资核算接口等功能,让它真正成为提升团队管理效率的得力工具。

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

运放频率补偿深度解析:从自激振荡到相位裕度的实战指南

做模拟电路的人&#xff0c;迟早都会撞上一个问题&#xff1a;放大器自激。明明原理图看着没问题&#xff0c;上电却开始振荡&#xff0c;示波器上全是毛刺&#xff0c;或者干脆整片电路变成射频振荡器&#xff0c;摸哪儿哪儿发热。我当年第一次用高速运放搭跟随器&#xff0c;…

作者头像 李华
网站建设 2026/8/26 20:47:31

基于STM32与半导体制冷片的DIY智能温控小冰箱全流程实战

1. 项目概述&#xff1a;从想法到实物的DIY制冷之旅 几年前&#xff0c;我在一个闷热的夏天突发奇想&#xff1a;能不能自己动手做一个能放在桌边、随时冰镇饮料的小冰箱&#xff1f;市面上虽然有迷你冰箱&#xff0c;但大多体积不小&#xff0c;噪音还大。于是&#xff0c;一个…

作者头像 李华
网站建设 2026/8/26 20:40:55

刷题笔记整理

刷题笔记 刷题程序的基础结构&#xff1a;public class Main{//标准的程序入口} 输入的使用&#xff1a; 1.导入java.util.*;工具包 2.创建一个Scanner扫描器的变量sc 3.读取整数&#xff0c;浮点数&#xff0c;字符串的代码格式 ● 读取整数&#xff1a; int n sc.next…

作者头像 李华
网站建设 2026/8/26 20:40:18

前端面试全攻略:核心维度与高频考点解析

1. 前端面试的核心考察维度 前端开发岗位的面试通常围绕四个核心维度展开&#xff1a;基础能力、框架理解、工程实践和项目经验。作为面试官&#xff0c;我设计题目时会刻意覆盖这些方面&#xff0c;确保全面评估候选人的技术水平。 基础能力部分主要考察HTML/CSS/JavaScript的…

作者头像 李华
网站建设 2026/8/26 20:37:52

数模竞赛中的区分度分析:从统计显著到工程落地的全链路实战

1. 项目概述&#xff1a;为什么区分度分析是数模竞赛里“被低估的硬功夫” 在数学建模竞赛现场&#xff0c;我见过太多队伍把90%精力花在模型搭建和代码调参上&#xff0c;最后却卡在“这个指标到底有没有统计意义”这一关——比如你用主成分分析降维后&#xff0c;发现前两个主…

作者头像 李华
网站建设 2026/8/26 20:37:15

Logback日志脱敏实战:从原理到实现,构建安全防线

1. 项目概述&#xff1a;为什么日志脱敏是开发者的必修课&#xff1f;最近在排查一个线上问题时&#xff0c;我翻看了几行应用日志&#xff0c;瞬间惊出一身冷汗。日志里明晃晃地记录着用户的手机号、身份证号&#xff0c;甚至还有一段未经处理的银行卡交易报文。这可不是危言耸…

作者头像 李华