公墓陵园管理系统源码解析:Spring Boot+MyBatis-Plus实现墓位状态机与合同缴费闭环 简介这份公墓陵园管理系统源码面向陵园运营单位、行业软件开发者及需要信息化管理方案的技术人员侧重销售提成管理与全面信息化可解决传统公墓管理软件操作复杂、管理不到位的痛点。系统为网络版采用C/S结构后台使用SQL Server 2008数据库具备较强的大数据处理性能与可扩充性。压缩包为rar格式共730个文件约3.31MB包含364个gif与74个png界面素材、66个aspx页面、81个cs与7个dll程序集、32个css与22个js前端脚本以及mdf、ldf数据库文件和sln解决方案等覆盖界面、业务逻辑与数据层。内容涉及墓位查询、销售录入、报表统计、碑文管理及WebService接口等模块目录结构完整便于二次开发与功能扩展。目前已有1817人学习下载适合作为行业管理系统的参考实现与开发脚手架。1. 公墓陵园管理系统从墓位建档到安葬流程一套源码把业务闭环跑通第一次拿到这个源码包时我以为就是个简单的墓位信息登记工具翻完目录才发现它把公墓陵园日常运营的整条链路都覆盖了——墓区规划、墓位建档、墓型定价、客户预约、安葬登记、费用缴纳、到期提醒、统计报表一个都没落下。这套系统适合两类人一是手里有陵园管理需求、想快速搭一套内部管理平台的开发者二是想拿一个业务逻辑完整、模块划分清晰的 Java Web 项目练手或做二次开发的人。技术栈是典型的 Spring Boot MyBatis-Plus MySQL Thymeleaf 组合前端用了 Layui 做后台界面部署简单改起来也顺手。下面我按实际拆包的顺序把架构、跑法、核心代码和改造点一个个讲清楚。2. 技术选型与目录结构为什么这套组合适合陵园业务2.1 后端选型逻辑公墓陵园管理系统的业务特点很明确数据关系复杂墓区→墓位→墓型→客户→合同→缴费记录但并发量不高查询和统计偏多写操作集中在预约和缴费环节。这种场景下Spring Boot MyBatis-Plus 是最省心的组合。MyBatis-Plus 自带的分页插件和条件构造器能省掉大量手写 SQL 的工作尤其是墓位查询这种多条件筛选按墓区、墓型、状态、价格区间用 LambdaQueryWrapper 几行就能拼出来。数据库选了 MySQL 5.7/8.0字符集用 utf8mb4因为客户姓名和备注里可能出现生僻字。连接池用的是 Druid监控页面在开发阶段排查慢查询很方便。权限控制没有上 Spring Security 那套重家伙而是用了轻量的拦截器 Session 方案对于内部管理系统来说够用也降低了二次开发的门槛。2.2 目录结构拆解源码包解压后是一个标准的 Maven 多模块结构我按实际目录列一下关键部分cemetery-management/ ├── cemetery-common/ # 通用工具、常量、返回体 ├── cemetery-dao/ # 实体类、Mapper 接口、XML ├── cemetery-service/ # 业务逻辑层 ├── cemetery-web/ # 控制器、页面、静态资源 │ ├── src/main/java/ │ │ └── com/cemetery/web/ │ │ ├── controller/ # 各模块 Controller │ │ ├── config/ # 拦截器、跨域、MyBatis 配置 │ │ └── vo/ # 前端展示对象 │ └── src/main/resources/ │ ├── templates/ # Thymeleaf 页面 │ ├── static/ # Layui、CSS、JS │ └── application.yml # 主配置 └── sql/ └── cemetery_init.sql # 建表 初始数据这个分层的好处是职责清晰dao 层只管数据映射service 层写业务规则web 层处理请求和页面渲染。二次开发时想加一个新模块照着现有模块复制一套就行不用动其他层。2.3 核心数据表设计陵园业务的核心是墓位和合同表设计直接决定了后续扩展的难易。我挑三张关键表说明表名作用关键字段cemetery_plot墓位信息id, area_id, plot_no, type_id, price, status, row_num, col_numcemetery_contract合同记录id, contract_no, customer_id, plot_id, sign_date, total_amount, statuscemetery_payment缴费流水id, contract_id, pay_amount, pay_date, pay_type, operator墓位状态用枚举值管理0-可售、1-已预定、2-已售出、3-已使用。这个状态机是整个系统的核心预约、签约、安葬三个环节分别对应状态流转后面讲代码时会重点说。3. 本地跑起来从建库到登录后台的完整步骤3.1 环境准备与数据库初始化先把环境对齐。JDK 用 1.8 或 11 都行Maven 3.6MySQL 5.7 以上。我习惯先用 Navicat 或命令行把库建好再导入 SQL 文件。# 登录 MySQL 后执行 CREATE DATABASE cemetery_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入初始化脚本脚本在 sql/ 目录下 mysql -u root -p cemetery_db sql/cemetery_init.sql导入完成后检查一下表数量正常应该有 15 张左右的表包括墓区、墓型、客户、合同、缴费、用户、角色等。初始数据里会带一个管理员账号密码是加密存储的第一次登录后建议立刻改掉。3.2 配置文件修改要点打开cemetery-web/src/main/resources/application.yml重点改这几处spring: datasource: url: jdbc:mysql://localhost:3306/cemetery_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发阶段关掉缓存改页面不用重启 prefix: classpath:/templates/ suffix: .html server: port: 8080 servlet: session: timeout: 30m # 内部系统会话时间可以设长一点serverTimezone这个参数别漏不然缴费日期可能差 8 小时对账时就是玄学问题。thymeleaf.cache在开发阶段设 false生产环境记得改回 true。3.3 启动与验证在项目根目录执行mvn clean package -DskipTests java -jar cemetery-web/target/cemetery-web-1.0.0.jar或者直接在 IDE 里跑CemeteryApplication主类。启动成功后访问http://localhost:8080会跳到登录页。用初始管理员账号登录能看到左侧菜单墓区管理、墓位管理、客户管理、合同管理、缴费管理、统计报表、系统设置。验证是否跑通按这个顺序点一遍先建一个墓区再在墓区下批量生成墓位然后建一个客户给客户签一份合同选一个墓位最后录一笔缴费。这四步走完没报错说明核心链路是通的。注意批量生成墓位时如果填的行列数太大比如 100×100页面会卡几秒因为后端是逐条插入。生产环境建议改成批量插入后面改造章节会讲。4. 核心模块拆解墓位状态机与合同缴费链路4.1 墓位状态流转的实现墓位状态是整个系统的骨架。可售→预定→售出→使用每一步都有业务约束只有可售的墓位能预定只有预定的墓位能签约已使用的墓位不能退。这套规则如果散落在各个 Controller 里后期维护就是血泪史。源码里把它收在 Service 层用状态机的方式管理。Service public class PlotServiceImpl extends ServiceImplPlotMapper, Plot implements PlotService { // 定义允许的状态流转当前状态 - 可流转到的目标状态 private static final MapInteger, ListInteger STATUS_FLOW new HashMap(); static { STATUS_FLOW.put(0, Arrays.asList(1)); // 可售 - 预定 STATUS_FLOW.put(1, Arrays.asList(0, 2)); // 预定 - 可售(取消) 或 售出 STATUS_FLOW.put(2, Arrays.asList(3)); // 售出 - 使用 STATUS_FLOW.put(3, Collections.emptyList()); // 使用后不可逆 } Override Transactional(rollbackFor Exception.class) public void changeStatus(Long plotId, Integer targetStatus, String operator) { Plot plot getById(plotId); if (plot null) { throw new BusinessException(墓位不存在); } ListInteger allowed STATUS_FLOW.get(plot.getStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new BusinessException(当前状态不允许此操作); } plot.setStatus(targetStatus); plot.setUpdateBy(operator); plot.setUpdateTime(new Date()); updateById(plot); } }这段代码的关键在STATUS_FLOW这个 Map它把业务规则显式化了。加新状态或改流转规则时只动这一个地方不用去翻各个 Controller。Transactional保证状态变更和后续操作比如生成合同在同一个事务里避免出现墓位已售但合同没生成的脏数据。参数说明plotId是墓位主键targetStatus是目标状态值operator记录操作人用于审计。实际调用时预约接口传 1签约接口传 2安葬登记传 3。4.2 合同与缴费的联动合同签约时会锁定墓位并生成应收金额缴费时核销应收。这两个动作必须原子化否则会出现合同签了但墓位没锁、或者钱收了但合同状态没更新的事故。Override Transactional(rollbackFor Exception.class) public Long signContract(ContractDTO dto) { // 1. 锁定墓位状态从预定改为售出 plotService.changeStatus(dto.getPlotId(), 2, dto.getOperator()); // 2. 生成合同编号日期 随机序列 String contractNo HT DateUtil.format(new Date(), yyyyMMdd) RandomUtil.randomNumbers(4); // 3. 保存合同主表 Contract contract new Contract(); contract.setContractNo(contractNo); contract.setCustomerId(dto.getCustomerId()); contract.setPlotId(dto.getPlotId()); contract.setTotalAmount(dto.getTotalAmount()); contract.setSignDate(new Date()); contract.setStatus(1); // 1-已签约 save(contract); // 4. 生成首期应收记录 PaymentPlan plan new PaymentPlan(); plan.setContractId(contract.getId()); plan.setShouldAmount(dto.getTotalAmount()); plan.setPaidAmount(BigDecimal.ZERO); plan.setStatus(0); // 0-未缴清 paymentPlanService.save(plan); return contract.getId(); }逻辑说明第一步先改墓位状态如果墓位状态不对会直接抛异常后面都不会执行。第二步生成合同编号格式是 HT日期4位随机数实际项目里如果合同量大建议改成数据库序列或 Redis 自增。第三步存合同第四步生成应收计划这样缴费模块就有依据了。缴费核销的代码逻辑类似核心是更新paid_amount并判断是否缴清缴清后把 plan 状态改为 1。这里有个细节金额用BigDecimal而不是 double陵园合同金额动辄几万用浮点数迟早出精度问题。4.3 统计报表的 SQL 写法报表模块是陵园管理者用得最多的功能源码里提供了墓位销售统计、缴费汇总、到期提醒三个报表。以墓位销售统计为例按墓区和墓型两个维度聚合select idselectSalesStat resultTypecom.cemetery.web.vo.SalesStatVO SELECT a.area_name AS areaName, t.type_name AS typeName, COUNT(CASE WHEN p.status IN (2,3) THEN 1 END) AS soldCount, COUNT(p.id) AS totalCount, ROUND(COUNT(CASE WHEN p.status IN (2,3) THEN 1 END) / COUNT(p.id) * 100, 2) AS soldRate FROM cemetery_plot p LEFT JOIN cemetery_area a ON p.area_id a.id LEFT JOIN cemetery_type t ON p.type_id t.id GROUP BY a.area_name, t.type_name ORDER BY a.area_name, t.type_name /select这个查询用CASE WHEN做条件计数一次扫描就能算出已售数量和总数比子查询效率高。soldRate是销售率管理者最关心的指标。如果数据量大建议在area_id和type_id上建联合索引。5. 避坑与常见问题部署和运行时的真实排查记录5.1 启动报错 Unknown database现象执行java -jar后控制台报Unknown database cemetery_db应用直接退出。原因数据库没建或者建库时名字拼错了。源码里默认库名是cemetery_db但有人建库时用了cemetery。解决先确认 MySQL 里SHOW DATABASES;能看到cemetery_db没有就重新执行建库语句。如果库名确实想改同步改application.yml里的 url。5.2 页面中文乱码现象墓位名称、客户姓名在页面上显示成问号或方块。原因数据库连接串没加characterEncodingutf8或者建库时字符集用了 latin1。解决检查application.yml的 url 参数确保有useUnicodetruecharacterEncodingutf8。如果库已经建错了执行ALTER DATABASE cemetery_db CHARACTER SET utf8mb4;再重新导入数据。5.3 登录后菜单空白现象能登录但左侧菜单不显示控制台报 403 或 404。原因静态资源路径没配对或者拦截器把静态资源也拦了。源码里拦截器配置在WebConfig中默认放行了/static/**和/layui/**。解决检查WebConfig的addInterceptors方法确认excludePathPatterns里包含了静态资源路径。如果自己改过目录结构同步改放行规则。5.4 批量生成墓位超时现象一次生成几百个墓位时请求转圈很久最后超时。原因Service 里是 for 循环逐条save()每条都走一次数据库连接。解决改成 MyBatis-Plus 的saveBatch()或者手动拼批量 INSERT。实测 500 条从 8 秒降到 1 秒以内。代码改动很小// 原来的写法 for (Plot plot : plotList) { save(plot); } // 改成 saveBatch(plotList, 500); // 每批 500 条5.5 缴费金额对不上现象合同总额和缴费流水加起来差几分钱。原因金额字段用了 double 或者 float累加时产生精度误差。解决所有金额字段统一用DECIMAL(12,2)Java 侧用BigDecimal并且BigDecimal比较用compareTo而不是equals。这个坑我在不止一个项目里踩过金融相关的字段千万别用浮点。6. 二次开发改造从单机版到多陵园 SaaS 的演进思路这套源码目前是单陵园部署模式所有数据在一个库里。如果你手里有多个陵园客户想改成 SaaS 版核心改造点有三个数据隔离、租户配置、报表聚合。数据隔离最省事的做法是在每张业务表加tenant_id字段然后用 MyBatis-Plus 的租户插件自动拼接条件。改造步骤先在cemetery_plot、cemetery_contract等表加tenant_id BIGINT然后在配置类里注册TenantLineInnerInterceptorBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 多租户插件自动在 SQL 中追加 tenant_id 条件 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { Override public Expression getTenantId() { // 从当前登录用户的 Session 或 ThreadLocal 中取租户 ID return new LongValue(TenantContext.getTenantId()); } Override public String getTenantIdColumn() { return tenant_id; } Override public boolean ignoreTable(String tableName) { // 系统配置表、字典表不需要隔离 return Arrays.asList(sys_config, sys_dict).contains(tableName); } })); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }这段代码的关键是TenantContext我一般用 ThreadLocal 在拦截器里从 Session 取出当前用户的租户 ID 存进去请求结束时清理。ignoreTable里把全局共享的表排除掉不然系统配置每个租户一份就乱了。第二个改造点是墓位编号规则。单陵园时编号可以简单用「区号行列」多陵园后要加租户前缀否则不同陵园的墓位编号会撞。建议在生成编号时拼上租户短码比如A01-001变成T01-A01-001。第三个改造点是报表。单陵园报表直接聚合就行多租户后要么每个租户单独查要么在 SQL 里按tenant_id分组。我倾向于前者因为租户之间数据本来就不该互相看见分开查更安全也方便做租户级别的数据导出。验证改造是否成功我会跑一个最小用例建两个租户各建一个墓区和一个墓位然后用租户 A 的账号登录确认只能看到 A 的数据用租户 B 登录只能看到 B 的。这个验证跑通基本就稳了。从那以后我每次改这种多租户项目都会先强制走一遍「双租户数据隔离验证」确认没有漏加tenant_id的表再往下做业务功能。希望这套源码的拆解能帮到你拿到包之后先按第 3 章的步骤跑通再对着第 4 章的状态机代码理解业务最后按第 6 章的思路做改造基本就能把这套陵园管理系统用起来了。本文还有配套的精品资源点击获取