做建筑工程的都知道,工地现场人员管理一直是老大难问题。工人分散在各个项目现场,靠微信语音汇报、纸质表格签字、月底反复对账,考勤和请假数据经常对不上。我前两年用Python Flask和微信小程序搭了一个建筑公司内部的员工请假考勤签到系统,今天把完整的设计思路、后端接口、小程序联调以及踩过的坑全部整理出来。这套方案不依赖第三方云服务,自己有一台服务器就能跑,适合中小型建筑企业、装修公司、劳务分包队做员工管理,前端用微信小程序实现,员工不用装额外App,打开微信就能用,管理者在后台直接审批请假、查看签到记录,实测下来比纸质流程至少节省一半沟通时间。
1. 项目概述与核心需求拆解
1.1 这个系统到底解决什么问题
建筑公司的考勤场景和普通办公室完全不一样。办公室员工固定工位、固定时间,打个卡就行;工地现场是多个项目并行,木工、瓦工、电工往往同时跨几个工地干活,项目经理要同时管理几十号人的出勤情况。传统的做法是每个工地放一本签到本,工人到了自己手写签名,月底由班组长统一汇总交给公司。问题很快暴露:字迹潦草看不清、漏签代签严重、请假信息不同步。有个真实案例是我朋友所在的劳务公司,月底核算工资时发现某工人考勤表上全勤,但实际上他请了三天假,项目经理和公司记录的完全对不上。这个系统就是为了解决这类信息断层——员工自己用手机发起请假申请,出勤打卡以手机端GPS定位和签到记录为准,数据实时同步到后台,管理层可以随时查看,每一个环节都有操作日志。
1.2 为什么选Flask加微信小程序,不选其他方案
技术选型上我几乎没有纠结。后端框架考虑的其实有三个方向:Django、Flask和Spring Boot。Django功能全但太重,自带Admin后台和ORM,对于这种轻量级内部工具反而显得臃肿;Spring Boot适合团队协作的大型项目,但需要Java环境,部署成本和维护成本都偏高。最后选了Flask,理由很直接:Flask足够轻量,一个主文件就能跑起来,配合SQLAlchemy做ORM,扩展性好,开发效率高。我整套系统从设计到上线用了不到两周,其中大部分时间花在小程序的UI调整上,后端接口写了一天半就全部完成了。小程序端选微信小程序而不是H5,是因为微信小程序有原生的登录能力,可以直接用微信授权获取用户信息,免去了账号密码注册的流程。建筑工人普遍年龄偏大,你让他们记一个复杂的登录密码根本不现实,让他们打开微信,点一下授权登录,门槛就低多了。
2. 系统架构与关键设计思路
2.1 整体架构:小程序端加后端接口加数据库
系统整体分三层,前端是微信小程序,中间是Flask提供的RESTful API,底层是MySQL数据库(也可以用SQLite快速起步,但生产环境建议MySQL)。微信小程序通过wx.request发送HTTPS请求到Flask接口,Flask处理业务逻辑后返回JSON数据。认证方案用JWT(JSON Web Token),用户在微信端调用wx.login拿到code,后端用code去微信服务器换取openid,再通过自定义登录接口获取JWT token。之后的每一次请求都在请求头中携带token,后端通过装饰器校验身份。我特意没做复杂的权限管理,只设计了两种角色:普通员工和管理员,管理员可以审批请假、查看全部考勤记录,普通员工只操作自己的页面。建筑公司通常没有细到部门层级,项目经理就是管理员,这种简化的权限模型足够用了。
2.2 核心功能模块设计
拆解下来核心模块有三个:请假申请模块、考勤打卡模块、签到管理模块。请假模块需要表单页,员工选择请假类型(事假、病假、年假、调休),填写起止日期和理由,提交后进入后台审批队列,管理员通过或驳回,状态变化通过微信模板消息通知到员工,虽然现在微信改版后订阅消息需要用户主动授权,但实现逻辑是一样的。考勤打卡模块解决"到底来没来"的问题,核心是GPS定位加时间戳,小程序通过wx.getLocation获取经纬度,后端比对工地坐标范围判断是否在允许的打卡范围内,如果在范围内记录为"正常",否则记录为"外勤打卡"由管理员确认。签到管理模块则是更细粒度的出勤记录,比如上午到场签到、下午离场签退,管理员可以按项目、按日期查看所有人员的签到情况。三个模块共用一套用户体系,员工的基本信息包括工号、姓名、手机号、所在项目,这些信息在上传第一张考勤照片或首次登录时完善。
3. 后端Flask核心实现详解
3.1 环境准备与项目初始化
本地开发环境我用的是Python 3.8,虚拟环境用venv创建。先安装基础依赖,requirements.txt文件内容如下:
flask==2.2.3 flask-sqlalchemy==3.0.3 flask-cors==4.0.0 pymysql==1.1.0 python-dotenv==1.0.0 pyjwt==2.6.0 requests==2.28.2初始化Flask应用时需要注意几点。应用实例创建时一定要把config配置分开写,不要让密钥、数据库地址硬编码在主文件中。我习惯用.env文件存储配置,用python-dotenv加载。另外要提前配置跨域,Flask端如果不在同一个域名下必然面临跨域问题。Flask-CORS库一行代码解决:
CORS(app)3.2 数据库模型设计
数据库设计直接决定业务逻辑的复杂度。三个核心表是用户表、请假表、考勤签到表。
用户表主要字段包括:id、openid(微信唯一标识)、employee_name(姓名)、employee_no(工号)、phone(手机号)、project_id(所属项目)、role(普通员工或管理员)、created_at。考勤签到表是数据量最大的表,每天的打卡记录都会写入一条,字段包含id、user_id、attendance_type(签到还是签退)、latitude、longitude、location_text(地址描述)、status(正常、外勤、迟到)、created_at。这里我把签到和签退都放在同一张表里,通过type字段区分,业务查询时按天分组处理,比单独建两张表灵活。请假表相对简单,字段有id、user_id、leave_type、start_date、end_date、reason、status(待审批、已通过、已驳回)、approver_id、created_at。
3.3 核心API接口开发
接口设计遵循RESTful风格,路径清晰。有几个核心接口:
POST /api/auth/login负责登录,接收小程序端传过来的code,后端用这个code去微信接口获取openid,通过openid查找或创建用户,返回token。这个接口必须正确处理首次登录的情况,如果用户不在数据库中就直接创建一条记录,给一个默认角色,后续再完善信息。
GET /api/attendance/status查询当前用户当天的考勤状态,返回今天有没有签到、有没有签退、当前时间等信息,前端小程序加载时先请求这个接口,用来判断应该显示"签到按钮"还是"签退按钮"。
POST /api/attendance/checkin接收经纬度和位置描述,后端判断时间——假如公司规定9点上班,9点30分前签到算正常,超过算迟到,迟到状态存入数据库。为了防作弊,我记录用户提交的定位与项目地址的距离,如果距离大于指定的200米,直接标记为"异常打卡"。
POST /api/leave/apply接收请假类型、起止时间、请假理由,状态初始为"待审批"。PUT /api/leave/{id}/approve是管理员审批接口,管理员请求时带上审批结果,后端更新状态,同时记录审批人ID。
每个接口都做了统一格式返回,业务前端只用判断code字段是否正确。返回格式统一为:
{ "code": 0, "message": "success", "data": {} }这样前后端联调时不用每个接口单独解析,省了很多事。
4. 微信小程序前端实现要点
4.1 页面结构与导航设计
小程序端我规划了四个Tab页面:首页(考勤打卡)、请假申请、考勤记录、我的(个人中心)。这四个Tab基本覆盖了员工的全部操作场景,没有多余的入口。页面结构上要注意微信小程序的顶部导航栏高度问题,尤其在做自定义导航栏的时候,不建议用全局自定义导航,因为有些安卓机顶部状态栏高度不一样,处理不当会导致内容被刘海屏遮挡。我用的方案是使用微信自带的navigationStyle,把navigationBarTitleText设置成项目名称,不做复杂改造。首页打卡页面最核心,顶部展示当前时间和日期,中间是一个大大的圆形签到按钮,下方显示最新的打卡记录。考勤记录页面使用日历组件展示一个月的出勤情况,日历中被标记为绿色的日期是正常出勤,橙色是迟到,红色是未打卡,一目了然。
4.2 核心功能页面开发
首页打卡是访问最频繁的页面。这类页面的关键点在于:一进入页面就不能让用户看到过时的状态,所以onShow生命周期内要重新请求考勤状态接口。签到按钮的交互要明确,签到成功后按钮立即变成灰色不可点击,并提示"今日已签到",避免重复提交。考勤记录页面我使用日历插件,我用的是vant-weapp的日历组件,它支持标记指定日期,配置很简单。请假申请页是一个表单页,注意日期选择器的时间范围限制:请假日期不能早于今天,结束日期不能早于开始日期,这些基础校验用小程序自带的picker组件实现。还有一个小细节是输入理由时如果字符太少要拦截,最少5个字,防止有人提交空白请假。
4.3 请求封装与数据联调
小程序和Flask后端联调时必须封装统一的request方法。我在utils/request.js里封装了一个请求函数,自动带上token,自动处理401跳转登录,自动弹出错误信息。同时要在微信开发者工具中开启"不校验合法域名"选项,因为本地开发时Flask跑在局域网ip上,域名是http协议,不校验才能联调。上线时必须配置HTTPS的合法域名。还有一个容易忽略的是wx.request的timeout,工地现场信号可能不太好,默认超时时间可以调整到10000毫秒,但不能太长,否则用户会以为卡死了。数据层面引入"加载更多"的分页功能,考勤记录和请假列表都采用分页加载,上拉触底时加载下一页数据,避免一次渲染几十条数据导致页面卡顿。这一点在低端安卓机上格外重要,亲测iPhone没问题,但几百块钱的红米手机渲染大量列表时会有明显的掉帧。
5. 实操过程:从零搭建完整系统
5.1 第一步:搭建Flask项目骨架
项目结构我按功能模块拆分,目录清晰好维护。我的目录结构大致是:
attendance_backend/ ├── app.py # 主入口 ├── config.py # 配置 ├── models/ │ ├── __init__.py │ ├── user.py │ ├── leave.py │ └── attendance.py ├── api/ │ ├── __init__.py │ ├── auth.py │ ├── attendance.py │ └── leave.py ├── utils/ │ ├── jwt_utils.py │ └── resp_utils.py └── requirements.txt主入口app.py里要做几件关键事:加载配置、初始化数据库、注册蓝图、启动CORS。代码大概是这样:
from flask import Flask from flask_cors import CORS from models import init_db from api.auth import auth_bp from api.attendance import attendance_bp from api.leave import leave_bp app = Flask(__name__) app.config.from_pyfile('config.py') CORS(app) init_db(app) app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(attendance_bp, url_prefix='/api/attendance') app.register_blueprint(leave_bp, url_prefix='/api/leave') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)5.2 第二步:配置数据库与ORM
生产环境我用MySQL,本地测试可以直接用SQLite,切换只需改一行连接字符串。SQLAlchemy在Flask中使用时,注意要在init_db(app)里执行db.create_all(),但更好的做法是提前把表结构生成好,避免每次启动应用都检查表存在。表结构变更时可以用Flask-Migrate管理迁移,如果项目工程量不大,直接用原生SQL建表也完全够用。我在models/user.py中写了User表的定义,openid字段设为可空,因为后续可能会支持账号密码登录,但核心登录方式仍是openid。
数据库连接池的问题容易被忽略。SQLAlchemy默认会维护连接池,但在服务器环境上如果连接闲置过久,MySQL会主动断开,可能导致报错"MySQL server has gone away"。解决办法是在engine配置中设置pool_recycle=3600,让连接每隔一小时回收一次,这个问题我在部署后第三天才遇到,线上跑了一会儿就报错,排查一圈才发现是这个原因。
5.3 第三步:开发核心API
登录接口是第一个要写的接口,逻辑也最关键。用Flask蓝图来组织接口,登录代码如下:
@auth_bp.route('/login', methods=['POST']) def login(): data = request.get_json() code = data.get('code') # 用code换openid openid = wx_code_to_openid(code) user = User.query.filter_by(openid=openid).first() if not user: user = User(openid=openid, role='employee') db.session.add(user) db.session.commit() token = generate_jwt(user.id, user.role) return success({'token': token, 'user': user.to_dict()})注意,wx_code_to_openid这个函数里要用微信官方接口:https://api.weixin.qq.com/sns/jscode2session 传入appid和secret换openid,这个请求必须放在后端执行,不能在客户端调,因为secret必须保密。打卡接口里需要做经纬度判断,这里我简化了逻辑,先把项目和经纬度配置存在配置文件中,计算用户坐标和项目坐标间的距离,大于指定阈值就标记异常:
@attendance_bp.route('/checkin', methods=['POST']) def checkin(): data = request.get_json() lat = float(data['lat']) lng = float(data['lng']) dist = haversine(lat, lng, PROJECT_LAT, PROJECT_LNG) if dist > 200: status = 'abnormal' else: status = 'normal' # 写入数据库计算两个经纬度之间的距离用的是haversine公式,不复杂,但精度足够判断一个工地范围。
5.4 第四步:创建微信小程序页面
我用的是原生微信小程序开发,不引入uni-app,因为这些页面交互不复杂,原生最直接。app.json里注册页面路径和tabBar,tabBar的颜色按公司logo配色设置。首页页面top部分显示日期时间,使用new Date()动态获取,每秒刷新一次时间显示,给员工直观的打卡反馈。中间打卡按钮采用大圆形设计,直径大概150rpx,点击时有个缩放动画效果,增加触感反馈。这些动效用CSS transition就能实现,不用额外引入动画库。请假页面用radio-group组件做请假类型的选择,picker做日期选择,textarea做理由输入。这些原生组件兼容性很好,在微信开发者工具和真机上表现一致。特别提醒一个细节:textarea在层级上有时会被原生组件盖住,微信官方提供了cover-view处理,但大部分场景下设置textarea的fixed属性为true就能解决问题,我在开发时就遇到输入法遮挡问题,调整后发现是placeholder位置的问题。
5.5 第五步:前后端联调与测试
联调阶段最重要的是抓请求和看response。微信开发者工具的网络面板可以直接查看请求详情,如果接口报错,先看后端日志,再看前端控制的报错内容。我习惯在每个接口的前端调用里加上console.log打印参数,把出问题的参数原样输出,方便对照。测试时要特别注意边界情况:请假时间跨周、打卡时间刚好卡在9点30分、网络断开后重新连接。我在测试中发现最严重的一个问题是重复提交,用户连续点击签到按钮会同时发出两个请求,导致数据库出现两条签到记录。解决办法是前端按钮加loading状态,提交后立即禁用按钮,同时后端也做了一层判断,查询当天是否已有签到记录,有就不允许再次插入,双重保险。
6. 常见问题与排查技巧实录
6.1 接口跨域问题
本地开发时Flask运行在localhost或局域网IP,小程序开发工具请求这些地址默认会跨域,我配置了Flask-CORS以后问题不大。但要注意,微信小程序的开发工具本地请求不需要关注CORS,因为小程序不是浏览器,没有同源策略的限制,只要是合法域名(开发模式可以跳过校验)就能访问。真正的坑在于上线后,必须把所有API域名配置成HTTPS且备案过的域名,否则扫码真机测试时会卡在域名校验失败。我踩过这个坑,第一次部署上线时用了自己服务器上未备案的域名,真机一直提示"request:fail url not in domain list",重新备案后等了5天才通过。所以建议提前处理好备案和SSL证书。
6.2 微信小程序包体积限制
微信小程序主包大小限制是2MB,如果代码或图片超了,会出现编译失败。这个项目本身不大,代码文件几十KB,完全没问题。但要注意别把头像和图片资源塞到包里,比如员工头像如果直接存base64字符串写入前端代码,很快就会超限。我的处理方式是把头像上传到服务器,数据库存图片路径,前端用image组件加载网络图片。另外,如果用了第三方组件库,比如vant-weapp,它会占几百KB的体积,没关系,还在2MB以内。但要提醒的是,vant组件是按需引入的,不要usingComponents里引入全部组件,否则体积会膨胀。
6.3 并发打卡与数据一致性
工地早晨上班时间段非常集中,7点50到8点05分之间可能同时有几十个人点击打卡。Flask默认的server是单线程的,并发请求会导致排队变慢,甚至超时。解决方法是部署时使用gunicorn作为生产服务器,并配置多个worker进程,例如gunicorn -w 4 -b 0.0.0.0:5000 app:app。数据库层面我加了唯一约束UNIQUE(user_id, attendance_type, date),保证一个人同一天不会插入重复的签到记录,但如果加了唯一约束,业务代码中就要处理IntegrityError异常,否则并发冲突时会直接报500。实践中我在打卡接口里做了try/except,捕获唯一约束冲突后返回"今日已签到"的提示,而不是报内部错误。
6.4 定位权限与精度问题
打卡功能依赖wx.getLocation,但微信小程序使用这个接口需要声明地理位置用途,而且用户必须手动授权。如果用户拒绝授权,就无法定位,打卡功能直接不可用,所以我做了权限检查,提示用户去设置页重新授权。精度问题上,GPS在室内或高楼附近漂移明显,实测在工地办公室里坐标可能飘到几百米外。我的策略是允许管理员在后台手工修正打卡记录,同时把打卡范围扩到300米范围,降低误判率。如果公司对打卡地点要求非常严格,可以配合WiFi签到或蓝牙Beacon方案,但那种情况下就要超出小程序范畴了,需要额外的硬件支持。
7. 经验扩展与后期优化建议
7.1 从考勤记录到工资核算的联动
目前这个系统只记录了考勤数据,没有和工资计算打通。实际上建筑公司的工资结构往往是底薪加日薪,请假扣款、加班补贴都直接和考勤明细挂钩。我规划中的下一步是增加一个薪资汇总表,后端根据每个月的请假记录和考勤天数自动生成工资明细,然后导出Excel给财务。这里涉及到一个计算逻辑:假设员工当月应出勤天数为22天,实际出勤天数为20天,按照日薪300元结算,请假两天扣除600元。如果事假和病假的扣款比例不同,请假类型字段就发挥作用了。这块功能不难,但能把系统价值提升一个档次,管理者月底不用把数据挨个复制到Excel里。
7.2 管理员后台的Web端扩展
目前管理员审批必须在微信小程序端操作,体验还凑合,但审批列表一多就不太方便了。后续考虑加一个独立的Web后台,用Vue加Element Admin实现,Flask端提供管理接口,权限要区分严格一些。管理后台能做更多事,比如批量导入员工、按项目筛选考勤数据、生成多维度的出勤率报表。我在设计Flask接口时已经预留了role字段,管理员接口用装饰器做了权限拦截,所以Web后台的接口完全可以直接复用,只需要前端重新写一套页面。这个扩展方向对于想要做成独立产品的团队很有参考价值。
7.3 消息通知与数据安全
消息服务建议接入微信的订阅消息,员工请假审批通过或驳回后给员工发送一条订阅消息提醒。但微信订阅消息现在是一次授权只能发一次,所以要在提交请假时让用户主动订阅,否则后续发不出去。数据安全方面,考勤数据属于员工隐私,后端日志中不能明文打印经纬度和手机号,我把坐标直接在应用层做了脱敏处理,只保留位置名称信息,定位经纬度不入日志。数据库也会定期备份,用crontab每天凌晨备份一次,防止服务器宕机导致考勤数据丢失。这些细节虽小,但要等到出问题再补就会被动了。
这篇文章我尽量把整个系统的设计和开发过程讲透了,从数据库表设计到小程序交互细节,再到部署运维中的各种坑,都是我实际做了一遍之后总结出来的经验。如果你也在计划做类似的考勤系统,最快的小路是先跑通Flask后端这几个接口,然后用微信开发者工具直接打开小程序目录联调,前端页面不求好看但求功能正确,等整体业务跑顺了再慢慢优化界面和交互体验。拿这套方案作为骨架,往里面添加会议室预约、加班申请、电子围栏等功能也都很方便,一次基础架构打牢,后续扩展就是加表和加接口的事了。