十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot的工地协同管理系统:考勤与环境监测设计实现

基于Spring Boot的工地协同管理系统:考勤与环境监测设计实现 工地上的考勤表还停留在纸质签字时代环境监测数据全靠人工巡检记录各个角色之间的信息沟通基本靠喊——这就是我当初做这个毕业设计时调研过的真实场景。基于Java的工地协同管理系统核心就是要把这些散落的环节集中到一个B/S架构的平台上统一管理。我最终实现的是一个面向工程项目的工人考勤与环境监测一体化系统后端采用Spring Boot构建RESTful接口前端通过浏览器访问考勤数据实时录入、监测数据自动采集、异常情况主动预警项目经理、安全员、施工班组长各角色按权限看到自己关心的内容。这篇博客把整个项目的设计思路、表结构、关键代码实现和踩坑经验完整记录下来适合正在做Java Web方向毕业设计、或者想了解B/S架构管理系统完整开发流程的同学参考。文章不会只贴代码更会把每个设计决策背后的理由讲清楚——为什么表要这样建为什么接口要这样设计为什么用这种方式处理并发打卡。1. 项目整体设计与技术选型分析1.1 工地协同管理到底要解决什么问题做系统开发的第一步永远是搞清楚业务痛点。我在调研中发现工地的管理场景和普通企业的OA系统有很大区别。首先是角色复杂一个工地上至少有项目经理、技术负责人、安全员、施工员、班组长、普通工人这几类人各自关注的数据完全不同。其次是场地分散工地不像是办公室没有固定的电脑工位绝大多数工人连智能手机都用不利索指望他们自己下载App打卡签到根本不现实。所以需求整理下来核心就是三块。第一块是工人考勤由班组长或者考勤员在系统里代操作记录每个工人的出勤情况月底自动统计工资结算的依据。第二块是环境监测工地现场的PM2.5、噪声、温湿度需要定时采集并记录这是文明施工和环保检查的硬性要求。第三块是信息协同公告通知、任务分配、问题上报需要有一个统一的渠道不然各个班组之间全靠微信群消息一刷就没了。系统的定位是B/S架构的管理后台前端不做复杂交互后端提供清晰的数据接口。这么定位的原因很实际工地管理人员习惯用电脑操作Excel表格浏览器访问的网页形式对他们来说学习成本最低同时作为毕业设计B/S架构也是评委老师最容易理解和认可的形式单体应用加上成熟框架稳定性有保障演示也方便。1.2 技术选型为什么是Spring Boot而不是SSH或SSMJava后端的技术选型其实经历过好几代。我身边有同学还在用SSHStruts2 Spring Hibernate也有用SSMSpring SpringMVC MyBatis的我最后选择了Spring Boot。原因很直接Spring Boot把大量繁琐的XML配置砍掉了内嵌Tomcat意味着打好Jar包就能跑对于毕业设计这种时间紧、重点是业务逻辑的项目来说省下来的时间可以投入到功能实现上。框架对比上是这样考虑的。Hibernate虽然全自动ORM看起来很省事但遇到复杂统计查询时写HQL或者Criteria反而绕。MyBatis的SQL由自己控制考勤按月汇总这种多表关联的报表查询写起来非常直观排查问题也容易。前端层面我用了Vue Element UI但对前端不熟练的同学完全可以用Thymeleaf模板引擎加Bootstrap同样能实现效果关键看自己的技术储备。还有一个重要的选型考量是权限管理。Spring Security功能强大但配置复杂Shiro相对轻量。我的项目里角色类型就那几种用拦截器加自定义注解的方式也能实现基本的权限控制。如果不想重复造轮子集成Sa-Token或者直接上Spring Security都可以选一个自己能讲清楚原理的就好因为答辩时老师一定会问权限是怎么控制的。2. 数据库设计与核心模块实现思路2.1 表结构设计——先想清楚业务再动手写代码数据库设计是这类管理系统最重要的一步。表结构没设计好后面写代码会各种别扭改表结构更是牵一发动全身。我的库名取为site_management核心表包括用户表、班组表、工人表、考勤记录表、考勤汇总表、监测设备表、环境监测数据表、公告表和任务表。用户表和工人表我做了分离设计。用户表存放的是系统登录账号和角色信息对应项目经理、安全员、考勤员这类操作后台的人工人表存放的是工地现场干活人员的基本信息工人本身不登录系统由考勤员代操作。两个表通过worker_user_id字段做关联这样设计的好处是工人照片、身份证号这类基础信息变更时不需要动账号数据灵活性更高。考勤记录表是核心中的核心我单独说明一下字段设计。表里包含worker_id工人ID、work_date日期、first_in_time上班打卡时间、last_out_time下班签退时间、status出勤状态正常/迟到/早退/缺勤、overtime_hours加班时长、operator_id操作员ID、remark备注。为什么要冗余一个operator_id因为代打卡的情况下需要追溯是谁录的这条记录出了纠纷有迹可查。环境监测这块设备表和监测数据表分开设计。设备表记录设备编号、设备类型噪声/PM2.5/温湿度、安装位置、所属区域监测数据表则是一张典型的时序表包含device_id、monitor_time、noise_value、pm25_value、temperature、humidity这些字段。实测下来这种设计在写入和查询时的表现都不错按时间和设备ID建索引后百万级数据量下查询依然流畅。2.2 工人考勤模块录入、统计与异常处理考勤模块的业务逻辑远比表面看起来复杂。工人的排班有常白班、两班倒、加班等多种情况考勤录入要考虑的是“一天的多次进出记录怎么处理”。我最终采取的策略是同一个工人同一天允许新增多条考勤记录但系统只取最早的一条作为上班时间、最晚的一条作为下班时间其余记录保留在操作日志中备查这样既保证了统计准确性又不丢失原始数据。迟到早退的判断规则我设计在Service层而不是数据库层。具体逻辑是上午9点之前打卡算正常超过9点算迟到下午6点之后打卡算正常下班早于6点离开算早退全天没有打卡记录则自动判定为缺勤。这些规则如果写死在SQL里后期调整规则就要改一堆Mapper放Service层用Java代码判断改动只涉及一个方法。报表统计按月进行。我额外建了一张考勤汇总表每月月底由定时任务跑批生成每个工人的出勤天数、迟到次数、早退次数、加班时长页面展示直接查汇总表而不是每次现算。这是一种典型的空间换时间思路。当时我对比过实时统计和汇总统计两种方案实时统计写SQL简单但数据量大后报表页面的等待时间会达到十几秒显然不能接受。2.3 环境监测模块数据采集、阈值预警与可视化环境监测模块在毕业设计里属于加分项但也是技术细节比较多的部分。工地的监测设备通过4G/网口把数据上报到服务器我的系统提供POST接口接收JSON格式的上报数据。接口设计需要考虑两个问题一是设备时钟不可靠上送的时间可能不准所以接收时必须用服务器时间校准二是网络闪断导致的重传同一设备同一时刻的数据可能上报多次接口要做幂等处理。我在数据表上加了unique约束device_id monitor_time重复上报时捕获DuplicateKeyException后直接忽略这个方案实现成本最低实测效果也很稳定。采集到的数据在页面端用ECharts绘制曲线图展示近24小时、近7天、近30天的趋势。为了让图表不卡顿按时间维度做了聚合查询——小时级别数据点用MAX函数聚合天级别用AVG函数聚合前端拿到的数据量被大大压缩。环境预警我用的是定时任务方案。每5分钟扫描一次最近一次上报的数据如果PM2.5超过设定阈值或者噪声超过分贝限制就自动生成一条预警记录并在首页醒目标出同时给安全员账号发送系统消息通知。预警阈值我做成可配置项存在系统参数表中不同工地可以设置不同的标准这样比硬编码更灵活。3. 实操过程与关键代码实现3.1 从零搭建Spring Boot后端项目后端项目的搭建现在非常简单直接到Spring Initializr网站生成基础工程或者用IDEA自带的Spring Initializr向导勾选Web、MyBatis、MySQL Driver这几个依赖即可。需要注意的是JDK版本和Spring Boot版本的兼容性我使用的是JDK 1.8 Spring Boot 2.7.x这是一套经过大量验证的稳定组合不建议用太新的版本给自己找麻烦。项目结构上我采用了经典的四层架构Controller层负责接口路由和参数校验Service层处理业务逻辑Mapper层DAO层负责数据库操作Entity层对应数据库表。除此之外还有一个common包存放统一返回结果类、异常处理类和工具类一个config包存放配置类。四层架构的好处是职责清晰答辩时也容易讲解。application.yml配置文件里核心是数据源和MyBatis的配置以下是一个配置参考。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/site_management?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.sitemanagement.entity configuration: map-underscore-to-camel-case: true这里有个关键配置是map-underscore-to-camel-case开启后数据库的snake_case字段会自动映射到Java的驼峰命名属性比如worker_id对应workerId省去大量手动映射的XML代码。另一个关键点是serverTimezone必须设置为Asia/Shanghai否则连接MySQL时会报时区错误这个坑几乎每个新手都会踩一次。3.2 考勤模块的后端实现示例考勤录入的Controller接口我设计为POST /attendance/record接收的参数包括workerId、workDate、inTime、outTime、status等。Service层的主要逻辑是校验当天是否已有记录如果有就追加操作日志没有则插入新记录。为了让代码可读性更好我在Service层加了事务注解确保操作日志和考勤记录要么同时成功要么同时失败。下面给出考勤记录插入和判重校验的关键代码这是整个考勤模块的核心逻辑。Service public class AttendanceServiceImpl implements AttendanceService { Autowired private AttendanceMapper attendanceMapper; Override Transactional(rollbackFor Exception.class) public Result saveAttendance(AttendanceRecord record) { // 校验同一天是否已有考勤记录 int count attendanceMapper.countByWorkerAndDate(record.getWorkerId(), record.getWorkDate()); if (count 0) { // 记录已有数据本次操作追加到日志表 attendanceLogMapper.insert(AttendanceLog.build(record, UPDATE)); // 更新为最晚下班时间和最早上班时间 attendanceMapper.updateTimeRange(record); return Result.success(考勤记录已更新); } // 首次录入直接插入 attendanceMapper.insert(record); return Result.success(考勤记录保存成功); } }考勤统计查询这边我使用了MyBatis的动态SQL来处理多条件组合查询。页面上的筛选条件可能有工人姓名、班组、月份、状态等多个维度如果给每种组合写一个SQL方法代码量会爆炸。动态SQL用 标签自动处理条件拼接为空的条件自动忽略代码简洁且灵活。select idselectAttendanceSummary resultTypemap SELECT w.name AS workerName, b.name AS teamName, COUNT(DISTINCT a.work_date) AS workDays, SUM(CASE WHEN a.status 迟到 THEN 1 ELSE 0 END) AS lateCount, SUM(CASE WHEN a.status 早退 THEN 1 ELSE 0 END) AS earlyLeaveCount, SUM(COALESCE(a.overtime_hours, 0)) AS totalOvertimeHours FROM attendance_record a INNER JOIN worker w ON a.worker_id w.id INNER JOIN team b ON w.team_id b.id where if testworkerName ! null and workerName ! AND w.name LIKE CONCAT(%, #{workerName}, %) /if if testteamId ! null AND w.team_id #{teamId} /if if testmonth ! null and month ! AND DATE_FORMAT(a.work_date, %Y-%m) #{month} /if /where GROUP BY w.id, w.name, b.name /select这段SQL的GROUP BY做了一个小优化。MySQL的ONLY_FULL_GROUP_BY模式下GROUP BY子句外的非聚合字段必须全部出现在GROUP BY中所以我把w.id也加了进去避免报错同时保证了同一名字的工人不会被合并统计。3.3 环境监测数据接收与主动预警实现设备数据上报接口的核心是参数校验和幂等去重。设备端使用HTTP POST请求Content-Type为application/json数据结构如下{ deviceId: DEV001, monitorTime: 2025-01-15 10:30:00, noise: 68.5, pm25: 85, temperature: 23.6, humidity: 45.2 }接收端的实现逻辑比较清晰第一步校验设备编号是否存在设备不存在直接返回错误码方便排查接入问题第二步将monitorTime与当前服务器时间比对超过5分钟视为无效数据丢弃第三步执行插入遇到唯一键冲突说明是重传数据直接忽略。以下是Controller层的关键代码。PostMapping(/api/device/report) public Result reportData(RequestBody MonitorData data) { // 校验设备是否存在 Device device deviceMapper.selectByDeviceId(data.getDeviceId()); if (device null) { return Result.error(1001, 设备不存在请检查deviceId); } // 校验数据时间有效性 if (Math.abs(System.currentTimeMillis() - data.getMonitorTime().getTime()) 5 * 60 * 1000) { return Result.error(1002, 数据时间异常上传失败); } try { data.setDeviceId(device.getId()); monitorDataMapper.insert(data); } catch (DuplicateKeyException e) { // 重复上报幂等处理直接返回成功 return Result.success(数据已上报); } return Result.success(上报成功); }定时预警的任务用Spring的Scheduled注解实现。我在启动类上加了EnableScheduling开启定时任务功能然后在预警类中定义每5分钟执行一次的job。预警任务查询最近一次各设备的上报数据与阈值配置比对超过阈值就调用预警记录插入方法并给相关角色发送通知。Component public class EnvironmentAlertTask { Autowired private MonitorDataMapper monitorDataMapper; Autowired private AlertRecordMapper alertRecordMapper; Autowired private NoticeService noticeService; Scheduled(fixedRate 300000) public void checkEnvironmentAlert() { ListDevice deviceList deviceMapper.selectAll(); for (Device device : deviceList) { MonitorData latest monitorDataMapper.selectLastByDeviceId(device.getId()); if (latest null) { continue; } // 读取阈值配置这里从参数表中获取 double pm25Threshold configMapper.getDoubleValue(pm25_threshold); if (latest.getPm25() pm25Threshold) { alertRecordMapper.insert(AlertRecord.build(device, PM2.5超标, latest.getPm25())); noticeService.sendNotice(安全员, 设备 device.getDeviceId() PM2.5超标); } } } }阈值配置我放在了config表中统一管理没有硬编码。这样做的原因是不同工地的环保标准不同有的项目对噪声要求特别严有的项目主要盯扬尘用配置项的方式让管理员可以自行调整比每次改代码重启好得多。3.4 前端页面与接口对接前端我用的是Vue 2 Element UI Axios的组合。页面结构上分为登录页、首页看板、考勤管理页录入、明细、报表、环境监测页实时数据、历史曲线、公告任务页、系统管理页。首页看板展示几个关键数据卡片——今日出勤人数、当前在场工人数、PM2.5实时值、待处理预警数量让管理员一登录就能掌握工地概况。Axios的封装有个细节值得注意我在请求拦截器中统一从localStorage取出token并放到请求头中在响应拦截器中统一处理HTTP 401未登录的情况直接跳转到登录页。如果每个接口都单独写认证逻辑代码会有大量重复而且遗漏任何一处都是安全隐患。环境监测曲线页是ECharts的核心应用场景。折线图的数据通过接口获取接口返回按小时聚合的数据点前端拿到后稍微做数据格式转换直接传给ECharts的series配置即可。聚合查询的逻辑放数据库里执行后端接口不需要做额外运算响应速度实测在100ms以内用户体验还不错。这里补充一个图表加载的技巧切换监测设备时先调用dispose销毁旧图表实例再初始化新实例否则会出现图表重叠或者数据不更新的诡异问题。4. 常见问题与排查技巧实录4.1 考勤记录重复、数据混乱的处理思路考勤模块上线实测阶段遇到最多的问题是重复录入。考勤员对同一个工人同一天录入了两次或者录完发现记错了再录一次导致统计报表中出勤天数虚高。最终的解决方案就是前面代码里展示过的Service层先查重已有记录则走更新逻辑而不是插入同时把每一次修改操作都记入日志。这个方案能从源头上卡住重复问题比事后人工清洗数据可靠得多。另一个常见问题是上下班时间判断错误。比如一个工人晚上加班到10点才走系统如果只取第一条和最后一条记录当晚上的加班记录被误删后最晚时间就变成了下午6点加班时长统计不准。我的处理方式是考勤员在录入时主动勾选“加班记录”标记系统把这个标记同步到考勤汇总表的overtime_hours字段不依赖打卡时间推算。宁可让操作员多点一下鼠标也不能让统计结果有歧义。4.2 环境监测数据量大、页面卡顿的优化方案环境监测数据是一张增长极快的表一个工地哪怕只有5台设备每5分钟上报一次一天就能产生1440条数据一个月就是4万多条一年接近50万条。数据量上来后第一次做全表查询时页面明显变卡MySQL的响应时间从几十毫秒涨到了两三秒。我做了三层优化。第一层是索引优化在monitor_time和device_id上建立联合索引查询条件只要带上设备ID和时间范围就能走索引扫描行数大幅减少。第二层是聚合查询历史曲线接口不再返回原始明细数据而是按小时、按天做聚合前端展示的数据点从几万个降到几百个渲染压力骤降。第三层是页面分页明细数据列表强制分页每页50条不允许一次性查询全部记录。这三层优化做完后接口响应时间稳定在几百毫秒以内。如果未来数据量继续增长到千万级别还可以考虑按月份分表存储但毕业设计阶段完全没有必要上这种方案过度设计也是问题。4.3 多角色权限控制的几种实现方案项目里有项目经理、安全员、考勤员、普通员工等多个角色权限设计的思路经历了两次迭代。最初版本只用前端页面做菜单级别的判断菜单根据角色隐藏或显示但这种方式安全性很差——懂点前端知识的人直接调用接口地址就能绕过限制访问数据。第二次迭代采用拦截器加角色校验的方式。用自定义注解标记接口需要的角色在拦截器中读取当前登录用户的角色信息比对不通过直接返回403拒绝访问。这种方式不需要引入庞大的权限框架原理也好讲清楚答辩时老师问起来能解释得明明白白。核心逻辑如下public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequireRole roleAnnotation handlerMethod.getMethodAnnotation(RequireRole.class); if (roleAnnotation ! null) { User currentUser (User) request.getSession().getAttribute(currentUser); if (currentUser null) { response.sendRedirect(/login); return false; } String requiredRole roleAnnotation.value(); if (!requiredRole.equals(currentUser.getRole())) { response.setStatus(403); return false; } } } return true; } }这个方案的扩展性不如Spring Security但对毕业设计这种角色固定、权限规则简单的场景完全够用。如果你想让项目显得更有技术含量可以集成Spring Security但一定要能讲清楚它的过滤器链和认证授权流程否则老师深入一问就容易露怯。4.4 项目部署与答辩准备的经验总结部署环节相对简单服务器安装JDK和MySQL后把Spring Boot打包成Jar包用nohup命令后台运行即可。环境变量别忘了设置JAVA_HOMEMySQL的编码要设置为utf8mb4而不是utf8否则中文可能出现乱码。打包命令使用mvn clean package -DskipTests跳过测试缩短打包时间。答辩的准备我认为有两个关键点。第一一定要能完整演示系统的核心业务流程从登录、录入考勤、查看报表到环境预警每个环节的演示时间控制在30秒以内全程流畅不卡壳第二对设计决策的原因要表达清楚比如问为什么用MyBatis不用Hibernate为什么考勤统计要用汇总表不用实时查询这些问题我在前面都给出了自己的思考答辩时就照着这个思路讲能体现你真的做过设计而不仅仅是抄代码。做这类管理系统最难的不是某个技术点的攻克而是把业务流程理解透然后落实到表和代码里。回头看我踩过的那些坑——时区报错、重复数据、图表不刷新、权限绕过每一个在解决后都让系统的质量上升了一个台阶。在写代码前多花一天时间把表结构画清楚在实现前多想想业务的异常情况这些习惯比任何一个框架技术都更有价值。
返回列表