最近好几个做毕设和刚转行做后端的朋友都在问同一个东西:基于SpringBoot的建筑工程项目管理系统。这确实是个很经典的选题——业务领域足够具体、功能边界清晰、技术栈主流,而且源码和讲解视频的配套资料也比较齐全,拿来学习或直接作为毕设交付都很合适。但很多人拿到源码后第一步就卡住了,不是跑不起来,就是不知道从哪看起。这篇内容我按实际开发的角度把它拆开讲:这系统到底解决什么问题、整体怎么设计、核心功能怎么做、跑起来之后会遇到哪些坑,一次性说清楚。
1. 项目概述:一套能直接交付的工程管理闭环系统
1.1 系统到底解决了什么问题
建筑工程的项目管理跟普通企业内部管理系统有个很大的区别:参与方多。甲方、施工方、监理方、材料供应商,各管一段,信息经常对不上。一个中型施工项目,从立项到竣工,中间的合同、进度、材料、设备、质量安全检查、施工日志,每一类数据都是分散的,很多工地今天还在用Excel表和纸质单据,最后的报表数据十有八九是对不齐的。
这个系统做的事情,就是把上面那套流程搬到线上:项目立项建卡、合同登记、材料出入库、进度计划填报、日常质量安全检查、施工日志记录,最后统一汇总成报表。所有数据在一个系统里流转,谁的权限能看到哪些内容,由角色控制。说得直白一点,就是给工程管理业务做了一套数字化的“工作台账”。
1.2 功能模块全景
整套系统按业务拆解,大致可以分成以下这些模块:
| 模块 | 核心功能 | 说明 |
|---|---|---|
| 系统管理 | 用户、角色、菜单、部门管理 | 基于RBAC的权限控制,不同角色看到不同菜单 |
| 项目管理 | 项目立项、项目信息维护、状态流转 | 项目状态分为筹备、在建、竣工、已结算 |
| 合同管理 | 合同登记、合同变更、付款记录 | 记录合同金额、签订时间、履约节点 |
| 材料管理 | 材料台账、入库登记、出库登记、库存查询 | 材料进出场记录,支持按批次追溯 |
| 进度管理 | 进度计划填报、实际进度反馈 | 计划和实际对比,方便发现滞后风险 |
| 质量安全 | 质量检查记录、安全隐患排查与整改 | 问题登记后需跟踪整改状态 |
| 施工日志 | 每日施工情况、人员机械情况记录 | 一线数据来源,是进度和统计的基础 |
| 报表统计 | 项目进度分布、材料消耗汇总、合同金额统计 | 图表展示,辅助管理决策 |
这套模块划分几乎覆盖了中小型建筑施工企业内部管理的全部核心业务。对于做毕设的同学来说,每块功能都能对应到数据库表和接口上,写论文和画架构图都有明确的落点。
1.3 适合谁参考
- 正在选题或已选这个题目的计算机相关专业学生,拿来对比自己的设计方案;
- 想通过一个完整项目入门SpringBoot全家桶的开发新人,重点是理解业务如何转成代码;
- 需要给小型施工队或项目部搭一套轻量管理工具、但不想从零设计的开发者。
这个项目最大的价值在于:它不是一个纯增删改查的“玩具系统”,而是有业务逻辑的——状态要流转、数据要关联、权限要收敛,这才像个真实项目的样子。
2. 整体设计与思路拆解:为什么这么选
2.1 为什么是SpringBoot而不是老一套
如果你去看三五年前的工程管理系统源码,大概率还是Spring MVC加配置文件堆出来的SSH(Spring + Struts + Hibernate)项目。现在选SpringBoot,最大的原因不是“新”,而是省事。
SpringBoot把配置变成约定:内嵌Tomcat,不用再单独打war包部署;自动装配会帮你把大部分常用组件的默认配置做好;起步依赖(starter)按场景引入,不需要自己管理一堆版本号。实际开发中,我只需要在pom.xml里加一个spring-boot-starter-web,再配合MyBatis-Plus做数据访问层,一个可运行的CRUD骨架就出来了。过去用SSH那套,光配数据源、事务、扫描路径就得折腾大半天。
另外,SpringBoot的社区生态太成熟了。后面要加权限控制有Spring Security和Sa-Token,文件存储有MinIO,做定时调度有Quartz或Spring自带的scheduling包,做消息队列有ActiveMQ和RabbitMQ——这些都是毕设里常见的功能扩展方向,选SpringBoot等于给自己留了足够多的后路。
2.2 单体架构还是微服务
很多同学一上来就问:要不要拆微服务?我一般先反问他一句:你的用户量多少?峰值并发多少?如果答案是“也就是一个公司几十号人用”“本地测试跑一跑”,那你拆微服务就是给自己找麻烦。
这个项目就是典型的单体应用:一个SpringBoot工程,内部按Controller、Service、Mapper分层。单体架构不是落后,而是在这个业务体量下最合理的方案——调试方便、部署简单、事务好控制。工程管理系统最重的操作就是材料出入库和合同付款记录,这类操作对数据一致性要求高,放在单体里一个事务注解就能保证,拆成微服务后反而要处理分布式事务,得不偿失。
如果非要给这个项目分模块,那就用Maven的module来做逻辑拆分:common放公共工具类,system放用户权限,business放工程业务。我只建议有洁癖的同学这么玩,功能上没有任何额外好处,但代码结构确实更清爽。
2.3 前后端分离,接口怎么定
目前网上公开的“SpringBoot + Vue”工程管理系统,绝大多数是前后端分离的。后端只提供JSON接口,前端是独立的Vue项目。这样做的好处是职责清晰:后端不用管页面渲染,前端可以单独启动和调试。
接口设计建议统一走RESTful风格,同时封装一个通用返回体,结构类似这样:
public class Result<T> { private Integer code; // 状态码,200成功,500失败 private String message; // 提示信息 private T data; // 业务数据 // 省略getter/setter和静态构造方法 }所有接口都返回这个结构,前端只需要在axios拦截器里统一处理code非200的情况,不用每次单独判断。分页接口统一返回PageResult<T>,里面带total和records字段。定好这个规范后,前后端可以并行开发,不需要一边开发一边为接口格式吵架。
2.4 权限模型的取舍
权限是管理系统绕不开的需求。这个项目建议用RBAC模型:用户属于角色,角色绑定菜单和按钮权限。具体实现上有两个方案:
方案一:Spring Security + JWT。这是最“标准”的解法。Spring Security的过滤器链在登录认证、接口鉴权上很完备,但学习曲线偏陡,尤其是自定义UserDetailsService和SecurityConfig里的过滤规则,新手经常会配错。
方案二:Sa-Token + 拦截器。这是我个人更推荐给毕设项目的方案。Sa-Token的功能和Spring Security类似,但API设计对新手友好得多——登录就调一行StpUtil.login(userId),鉴权就加@SaCheckPermission("system:user:add")注解。省下来的时间足够你把业务模块做得更完整。
不管选哪个方案,核心逻辑都是一样的:登录成功后签发token,前端请求头带上token,后端解析token取出当前用户,再判断用户有没有访问该接口的权限。
3. 核心细节解析与实操要点
3.1 数据库设计:先想清楚表关系和状态流转
数据库设计决定项目的天花板。我见过不少同学一上来就写代码,做到材料模块才发现不知道库存怎么算,就是因为没提前设计好表结构。
以下是最核心的几张表,字段我按实际开发经验做了精简:
| 表名 | 用途 | 关键字段 |
|---|---|---|
sys_user | 系统用户 | id, username, password, real_name, dept_id, status |
sys_role | 角色表 | id, role_code, role_name, remark |
sys_menu | 菜单权限表 | id, parent_id, menu_name, path, perms |
project_info | 项目信息表 | id, project_code, project_name, address, manager_id, status, start_date, end_date, budget |
project_contract | 合同表 | id, contract_no, project_id, amount, sign_date, party_b, status |
material_info | 材料基本信息表 | id, material_code, material_name, spec, unit, price |
material_stock_record | 材料出入库记录表 | id, material_id, project_id, record_type, quantity, operator_id, create_time |
schedule_plan | 进度计划表 | id, project_id, plan_name, plan_start, plan_end, actual_start, actual_end, progress, status |
quality_check | 质量安全检查表 | id, project_id, check_type, check_date, inspector_id, rectification_status, rectification_date, detail |
construction_log | 施工日志表 | id, project_id, log_date, weather, content, people_count, machine_info, create_by |
特别注意一个点:材料出入库不要直接改材料表的库存字段,而是通过流水记录推算库存。也就是说material_info表里可以不存库存数量,查询库存时对material_stock_record做SUM聚合。这么做的好处是每次出入库都有据可查,出现对账差异时可以直接定位是哪条流水出了问题。
3.2 权限设计:注意“数据权限”这个隐藏需求
菜单权限解决的是“能不能点这个功能”,但工程管理里更头疼的是“能看哪些项目的数”。比如:施工员A只负责项目甲,他就不该看到项目乙的合同信息。
实现方式很简单:在项目表里维护manager_id和create_by字段,查询项目列表时根据当前用户的角色做条件拼接:
// 伪代码逻辑:项目经理只查自己负责的项目 if ("PROJECT_MANAGER".equals(currentRoleCode)) { lambdaQueryWrapper.eq(ProjectInfo::getManagerId, currentUserId); }这个是评审老师非常喜欢追问的点——权限只做到菜单这一层只能叫“能用”,做到数据这一层才叫“合理”。写论文的时候一定要把这一块单独拉出来写。
3.3 文件上传:MinIO比本地文件夹靠谱
工程管理系统经常会涉及上传文件的需求:合同扫描件、质量检查照片、材料合格证。把文件存在本地磁盘的upload/目录里虽然简单,但存在两个问题:一是应用多实例部署时文件不同步,二是重启或重新部署时文件容易丢。
更好的方案是集成MinIO。MinIO是一个开源的对象存储服务,兼容Amazon S3 API,部署特别简单,下载一个二进制文件跑起来就能用。SpringBoot里集成它的步骤也不复杂:引入minio依赖,配置endpoint、accessKey、secretKey、bucketName,然后写两个工具方法:一个是上传,一个是生成访问URL。
@Configuration public class MinioConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }注意:如果是本地测试环境,浏览器访问MinIO里的文件可能会遇到跨域问题,需要在MinIO控制台配置好Bucket的Access Policy为
readonly,或者通过后端接口做文件流代理转发。
3.4 图表报表:别在SQL里做太多事
项目进度、材料消耗、合同金额统计这类功能,前端通常用ECharts展示。这里最容易犯的错是想着用一条SQL把所有维度都查出来,结果写出一个几百行的SELECT嵌套子查询,既跑得慢又难维护。
我的建议是:报表统计按维度拆成多个轻量查询。比如柱状图展示“各项目合同金额”,就只查project_contract按项目分组求和;饼图展示“材料分类占比”,就只查material_info按分类分组计数。然后再在前端把多个接口的数据组装到ECharts的option里。后端查询简单了,前端展示灵活了,性能也完全够用。
4. 实操过程与核心环节实现
4.1 从零搭建项目骨架
不管你是拿到别人的源码还是自己写,第一步都是先把工程跑起来。创建的步骤我重新走一遍,给没经验的同学做个参考:
第一步,生成基础工程。打开Spring Initializr(直接用IDEA内置的也行),选择Java版本(建议8或11,别选17以上,免得跟教程里的依赖版本对不上),Group填com.example,Artifact填project-management。依赖先只勾Spring Web,其他的后面在pom.xml里手动加。
第二步,引入核心依赖。除了spring-boot-starter-web,还需要这些东西:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.37.0</version> </dependency>第三步,配置application.yml。核心是数据源和MyBatis-Plus的配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/project_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 50MB mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0提示:
map-underscore-to-camel-case一定要开,不然数据库里的project_name映射不到实体类的projectName字段上,查出来的数据全是null。
第四步,写一个测试接口验证工程。建一个TestController,里面放个hello()方法,启动项目后访问http://localhost:8080/hello,能看到返回结果就说明骨架通了。
4.2 登录认证与当前用户获取
登录是整个系统的入口。基于Sa-Token的实现方式很简洁:
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private SysUserService sysUserService; @PostMapping("/login") public Result<String> login(@RequestBody LoginRequest request) { // 1. 根据用户名查询用户 SysUser user = sysUserService.lambdaQuery() .eq(SysUser::getUsername, request.getUsername()) .one(); // 2. 校验密码(这里用MD5演示,生产环境建议BCrypt) if (user == null || !Md5Util.matches(request.getPassword(), user.getPassword())) { return Result.fail("用户名或密码错误"); } // 3. 判断账号是否被禁用 if (!"1".equals(user.getStatus())) { return Result.fail("账号已被禁用"); } // 4. 登录,签发token StpUtil.login(user.getId()); return Result.success(StpUtil.getTokenValue()); } }拿到token之后,后续所有请求在请求头里带token字段即可。需要获取当前登录用户时,用StpUtil.getLoginId()取到用户ID,再去查用户信息。
4.3 项目立项与状态流转
项目立项是整个工程业务的开端。这个功能看起来简单,实际写起来有几个容易被忽略的细节:
第一,项目编码要唯一且自动生成。不要让人手工输入,直接用时间戳加随机数生成:PRJ + yyyyMMdd + 三位随机数字。生成时要做重复校验,防止并发下撞号。
第二,立项时要同时初始化关联数据。比如项目负责人权限关联、进度计划的初始节点、材料台账的空壳记录。我见过很多系统只插了一条project_info,后面做报表时各种空指针。
第三,状态流转要做控制。项目状态从“筹备”到“在建”再到“竣工”,不能随便跳。最简单的方式是在Service层写状态流转方法,比如startProject(Long projectId)方法里检查当前状态必须是“筹备”,然后再更新。
public void startProject(Long projectId) { ProjectInfo project = getById(projectId); if (!"1".equals(project.getStatus())) { throw new BizException("当前状态不能开工"); } project.setStatus("2"); // 2=在建 project.setActualStartDate(LocalDate.now()); updateById(project); }4.4 材料出入库:事务和库存计算
材料管理模块是业务逻辑最重的部分。入库和出库都涉及两步操作:插入流水记录、更新相关项目数据。这两步必须放在一个事务里,否则就会出现“流水有了但项目统计没更新”的不一致问题。
@Transactional(rollbackFor = Exception.class) public void stockIn(StockRecordDTO dto) { // 1. 保存原材料入库流水 MaterialStockRecord record = new MaterialStockRecord(); record.setMaterialId(dto.getMaterialId()); record.setProjectId(dto.getProjectId()); record.setRecordType("IN"); record.setQuantity(dto.getQuantity()); record.setOperatorId(StpUtil.getLoginIdAsLong()); record.setCreateTime(LocalDateTime.now()); save(record); // 2. 更新材料台账表的累计数量 materialService.lambdaUpdate() .eq(MaterialInfo::getId, dto.getMaterialId()) .setSql("total_quantity = total_quantity + " + dto.getQuantity()) .update(); // 3. 记录日志 logService.record("材料入库", dto.getMaterialId(), dto.getQuantity()); }注意:
@Transactional一定要加rollbackFor = Exception.class,因为Spring默认只在抛出RuntimeException时才回滚,如果方法里抛出的是自定义检查异常,事务是不会回滚的,这个坑很容易踩。
库存查询不直接查total_quantity字段,而是把入库流水求和减去出库流水求和:
public BigDecimal getStock(Long materialId) { BigDecimal totalIn = stockRecordService.lambdaQuery() .eq(MaterialStockRecord::getMaterialId, materialId) .eq(MaterialStockRecord::getRecordType, "IN") .list().stream() .map(MaterialStockRecord::getQuantity) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal totalOut = stockRecordService.lambdaQuery() .eq(MaterialStockRecord::getMaterialId, materialId) .eq(MaterialStockRecord::getRecordType, "OUT") .list().stream() .map(MaterialStockRecord::getQuantity) .reduce(BigDecimal.ZERO, BigDecimal::add); return totalIn.subtract(totalOut); }4.5 分页查询与条件筛选
很多业务列表页面都需要分页加条件筛选,MyBatis-Plus写起来很快。先注册分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后Service里用LambdaQueryWrapper拼条件:
public PageResult<ProjectInfo> pageProjects(ProjectQueryDTO query) { LambdaQueryWrapper<ProjectInfo> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(query.getProjectName())) { wrapper.like(ProjectInfo::getProjectName, query.getProjectName()); } if (StringUtils.hasText(query.getStatus())) { wrapper.eq(ProjectInfo::getStatus, query.getStatus()); } if (query.getManagerId() != null) { wrapper.eq(ProjectInfo::getManagerId, query.getManagerId()); } wrapper.orderByDesc(ProjectInfo::getCreateTime); Page<ProjectInfo> page = new Page<>(query.getPageNum(), query.getPageSize()); Page<ProjectInfo> result = page(page, wrapper); return new PageResult<>(result.getTotal(), result.getRecords()); }5. 常见问题与排查技巧实录
写代码的过程不可能一帆风顺,下面这些坑是我在这个类型的项目里见过最高频的,整理成速查表,遇到直接照着排查。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 项目启动失败,提示端口被占用 | 本地8080端口被其他程序占用 | 换端口或杀掉占用进程:`netstat -ano |
查询数据库报Unknown column 'xxx' | 实体类字段和表字段没对应上 | 检查map-underscore-to-camel-case是否开启,或加@TableField注解 |
| 分页查询返回全量数据 | MyBatis-Plus分页插件没注册 | 检查是否添加了MybatisPlusInterceptor配置 |
| 登录后直接返回JSON比拿到页面 | 请求路径写错,拦截器把登录接口拦截了 | 在Sa-Token配置中放行/api/auth/login和/api/auth/captcha |
上传文件时报Current request is not a multipart request | 前端没设置Content-Type: multipart/form-data | 检查前端请求头,或换用axios的FormData方式提交 |
接口报跨域错误CORS | 后端未配置跨域 | 写一个CorsConfig,放行Vue开发服务器的地址 |
时间字段显示格式不对,带T字母 | Jackson默认序列化格式问题 | 在application.yml里设置spring.jackson.date-format,实体类日期字段加@JsonFormat |
| 列表页加载很慢 | 关联查询太多,N+1问题 | 用MyBatis-Plus的selectBatchIds批量查关联数据,避免循环单查 |
| 修改数据不生效但没报错 | 实体类里没填ID,updateById更新不了 | 查日志确认主键是否传参,或在SQL里用updatewrapper按条件更新 |
除此之外,有几个实操心得特别想说:
源码拿到手先别急着看代码,第一步永远是看数据库脚本。把建表SQL导进MySQL,再用show tables看一下表结构,动手画一遍表之间的关系图。只要表关系理清了,代码里再复杂的方法调用,本质都是在给这些表做增删改查。
不要在Controller里写业务逻辑。我见过有不少源码把SQL查询、条件判断、权限校验全堆在Controller方法里,一个方法几百行。这种代码跑能跑,但答辩的时候老师一眼就知道你对分层设计理解不深。正确的做法是Controller只负责参数接收和结果返回,业务逻辑全部下沉到Service。
手工插入几条测试数据时,记得把逻辑删除和创建时间字段一起填好。很多MySQL表都设置了deleted字段做逻辑删除,如果你插入数据时忽略了这个字段,列表查询时会默认过滤掉,你会以为代码有Bug,实际上只是数据没插对。
最后,尤其中重要的一点:文件上传目录和MinIO地址不要在代码里写死。遇到别人运行你的项目,他本地没有你的D:/upload目录,文件功能立刻报错。要把这类路径放到application.yml里,让别人下载源码后改一处配置就能跑起来。这一点非常影响别人对你代码质量的评价。
6. 项目扩展方向:答辩或面试的加分项
如果这个系统是你拿来毕业设计或作为项目经历去面试的,我强烈建议你在核心功能跑通之后再补以下几个扩展点,投入的时间不多,但价值很明显:
扩展一:Excel批量导入导出。用EasyExcel把项目清单、材料台账导出成Excel,或者从Excel批量导入合同台账。这个功能在工程管理场景里极其实用,没有哪个管理员愿意一条一条往系统里敲合同信息。
扩展二:待办消息提醒。合同快到期了、安全检查整改日期到了、材料库存低于某个阀值,系统能自动生成待办消息。实现上可以用Spring的@Scheduled定时任务去扫描相关表,生成待办后存库,用户登录后在首页看到待办数量角标。
扩展三:数据库定时备份。工程管理系统数据价值高,数据库备份是刚需。可以用@Scheduled(cron = "0 0 2 * * ?")每天凌晨两点执行一次mysqldump,并把备份文件传到MinIO保存。这个功能写不了多少代码,但在项目介绍里出现“数据安全”关键词,会比你写一百行普通CRUD更容易打动面试官。
扩展四:移动端适配。施工员在工地现场很少开电脑,他们使用最多的场景是现场拍照上传检查记录、填报施工日志。你可以做个简单的H5页面,或者用Vue的响应式布局把几个核心页面适配到手机浏览器。这个方向延伸出去就是“移动工程管理”,主题瞬间高级不少。
我在实际开发这个类型的项目时,最大的体会就是:写业务功能本身其实不难,难的是对整个项目生命周期有全局的预判——表结构怎么设计才能支撑未来的报表统计、权限怎么控制才能满足不同角色的数据隔离、使用过程中哪些非功能需求(比如备份、文件存储、消息提醒)才是用户真正离不开的。把这些想清楚了,写出来的代码才算是一个“系统”,而不是一堆接口堆在一起。