每年做毕业设计选型的时候,"不动产登记系统"这类题目几乎是常青树。但说实话,能把这个题目做好的人不多。很多人一听"不动产登记",就以为是在写一个简单的房屋信息增删改查,结果做出来的系统既没有体现登记业务的核心逻辑,也没有分层架构的味道,答辩时被老师问两句就露馅了。
我在带毕业设计的过程中,帮学生重构过好几套类似项目。这里我以SpringBoot + Vue为核心,完整回顾一套"不动产权籍信息管理平台"的设计与实现思路。它包含了权籍基础数据管理、登记业务流转、租赁合同管理、权限控制、定时提醒、统计图表这些完整闭环,同时也足够贴合"前后端分离架构"这个答辩高频词。如果你正在准备这类选题,或者已经写了半截但不知道怎么往下推,这篇文章应该能给你不少可直接抄的作业。
1. 为什么选不动产这条赛道:毕设选题的业务拆解思路
1.1 不动产权籍到底管的是什么
先不说代码,先把业务搞明白。很多人栽在这个题上,就是栽在"不知道自己要管理的数据是什么"。
"权籍"这个词,拆开就是"权属"和"簿籍"。往浅了说,它回答的是两个问题:这套房子是谁的?依据是什么?往深了说,这是一套以不动产单元为核心的数据体系,包含坐落位置、建筑面积、用途、权利类型、权利人信息,以及登记业务办理过程中产生的各种申请与审批记录。
举个例子:一套商品房,它的基本信息是"不动产单元号 + 坐落 + 建筑面积 + 房屋用途",然后关联一个或多个权利人。这套房子里发生过一手房买卖、抵押贷款、租赁备案,这些业务都是在"不动产权籍"这张大网下延伸出来的。所以你不能只建一张"房屋表"和一张"业主表"草草了事,而是要以不动产单元为主轴,把所有业务记录挂在它下面。
1.2 我圈定的核心业务范围
完整的不动产登记系统是一个庞大的政务系统,涉及预告、首次、变更、转移、注销、更正等多种登记类型,背后还要对接税务、住建、测绘。毕设项目不需要也不可能做这么全,但你得有取舍的判断力。
我这套系统的定位是:面向权籍管理与登记业务办理的简化型管理平台。具体圈定了以下几块:
- 系统管理:用户、角色、菜单权限。
- 权利人档案管理:录入、修改、查询权利人信息。
- 不动产单元管理:管理自然幢、逻辑幢、房屋登记信息,维护不动产单元号。
- 登记业务办理:覆盖首次登记、变更登记、转移登记、预告登记、抵押登记,流程上采用"受理 -> 审核 -> 登簿"三段式。
- 租赁合同管理:登记房屋出租信息,维护承租人、租金、租期,并做到期提醒。
- 首页统计看板:登记业务量、登记类型分布、租赁到期数量等。
判断一个业务该不该纳入毕设,我的标准很简单:它要么能体现数据模型的设计能力,要么能体现流程控制的能力,要么能体现工程化能力(比如定时、权限、图表)。纯粹的字段增删改查,能砍就砍。
1.3 用户角色与权限边界
这套系统设计了三个角色:
| 角色 | 核心权限 |
|---|---|
| 系统管理员 | 用户管理、角色权限分配、菜单配置、数据维护 |
| 登记业务员 | 受理登记申请、录入权籍信息、草拟登记结果 |
| 查询用户 | 查询不动产单元信息、权利人信息、租赁台账 |
为什么强调角色?因为登记业务有明确的内部制衡要求,业务员不能直接把一条登记申请置为"登簿",必须经过审核环节。这个逻辑在答辩时非常加分,你可以明确告诉老师:权限体系不是装饰,它呼应了不动产登记的合规性要求。
2. 技术栈选型背后的取舍:不是追求流行,而是追求稳妥
2.1 SpringBoot版本、JDK与数据持久层
先说后端。当前毕业设计的主流组合里,SpringBoot 2.7.x + JDK 8 仍然是最稳的选择,不要因为网上全是SpringBoot 3的教程就盲目追新。SpringBoot 3要求JDK 17,且部分第三方组件的兼容性调整较多,对时间紧张的毕设来说不划算。如果你非要上3.x,那至少得预留一周时间处理组件适配问题。
持久层我用了MyBatis-Plus,理由非常直接:
- 单表CRUD零SQL,省掉大量机械编码时间。
- 自带分页插件,分页查询不用手写Limit逻辑。
- 逻辑删除、自动填充这些功能挺好用,适合写进"工程化能力"的答辩话术里。
配合SpringBoot的项目结构,标准的包划分如下:
com.example.realestate ├── controller ├── service ├── mapper ├── entity ├── dto ├── vo ├── config ├── common └── utilsController层只做参数接收和结果封装,一切业务判断下沉到Service层。这样分层的目的不是美观,而是答辩时你可以指着任意一个类说清楚"它属于哪一层、为什么在这一层"。我见过太多学生把SQL直接写在Controller里,那根本不叫分层。
2.2 前端技术栈:Vue 3 + Vite + Element Plus
前端我选了Vue 3的组合式API,配合Vite构建、Element Plus组件库、Pinia做状态存储、Vue Router做路由、Axios发请求、ECharts画图。
这里要说道说道Vue 2还是Vue 3的问题。Vue 2的教程确实多,Element UI的答案也遍地都是,但它已经进入了维护末期。Vue 3的回答题(比如ref和reactive的区别、setup语法糖、Pinia与Vuex的对比)在答辩时更容易展开。我建议直接用Vue 3,遇到问题查"Vue3 + Vite + 对应组件名"即可,资料已经足够多了。
环境配置层面的坑非常集中,第一批倒在初始化上的学生,大多是Node版本不对。Vite 5要求Node 18+,而很多机器默认还是16。安装Vue依赖时一旦报错,先执行node -v,再执行npm -v,确认版本后再决定是否升级。
2.3 前后端分离架构到底好在哪
这套系统坚持前后端分离是有原因的。后端纯提供RESTful API,前端纯做展示和交互。两边可以并行开发,也可以独立部署。最直接的好处是,前端打包后可以交给Nginx托管,后端SpringBoot独立跑在8080端口,数据通过JSON交换。
答辩的时候,老师最常见的问题就是"为什么要用前后端分离,而不是传统的服务端渲染?"。你要能说清楚三个点:开发职责分离、API复用性强、部署灵活。这三个点后面我会在部署部分具体演示。
3. 数据库建模:登记系统的根在权籍基础数据
3.1 数据模型分两层:基础档案与业务流水
不动产登记系统的数据库设计,我习惯分成两层来看。
第一层是基础档案层,也叫主数据层。它回答的是"现在有哪些房子、哪些人"。核心表包括:
sys_user:系统用户。sys_role:角色表。user_role:用户角色关联表。owner_info:权利人档案。estate_unit:不动产单元表。
第二层是业务流水层,它记录的是"发生了什么业务、办到哪一步了"。核心表包括:
register_bill:登记业务主表。register_step_log:登记流程日志表。lease_contract:租赁合同表。
为什么必须区分这两层?因为业务流水是会不断增长的,而基础档案相对稳定。如果你把登记信息直接塞到不动产单元表里,表结构会被不断变更的需求撑爆,后期查询也乱成一团。
3.2 核心表字段设计思路
estate_unit(不动产单元表)是整套权籍模型的锚点,字段设计上要体现不动产单元号的唯一性。我给的简化设计如下:
CREATE TABLE estate_unit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, unit_code VARCHAR(64) NOT NULL COMMENT '不动产单元号', building_name VARCHAR(128) COMMENT '楼幢名称', location_desc VARCHAR(255) COMMENT '坐落位置', area DECIMAL(10,2) COMMENT '建筑面积', property_use VARCHAR(32) COMMENT '用途:住宅/商业/办公等', status TINYINT DEFAULT 1 COMMENT '1-正常 0-已注销', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_unit_code (unit_code) ) COMMENT '不动产单元表';register_bill(登记申请主表)则要记录"是谁、在什么时间、为哪套不动产、申请办什么事、现在办到哪个环节"。核心字段包括登记类型、当前状态、申请人ID、不动产单元ID、申请时间、办理意见。这里强调一个设计细节:登记类型用字典值管理,不要写死在业务代码里。
CREATE TABLE register_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(32) NOT NULL COMMENT '登记业务编号', register_type VARCHAR(32) COMMENT '登记类型:first/change/transfer/...', estate_unit_id BIGINT COMMENT '不动产单元ID', applicant_id BIGINT COMMENT '申请人ID', status TINYINT COMMENT '0-草稿 1-受理 2-审核通过 3-登簿 4-驳回', current_handler VARCHAR(64) COMMENT '当前办理人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_bill_no (bill_no) ) COMMENT '登记业务主表';另一个容易被忽视的register_step_log表非常关键。它记录每一次状态变更的操作人、操作时间、操作动作和意见。很多同学不做这张表,答辩时一旦被问到"如何追溯一条登记业务的办理历史"就会卡住。
3.3 登记状态流转不是外键能解决的
很多学生习惯用外键去表达业务关系,比如在子表里加个parent_id。但登记业务的"流转"是有方向、有时序、有条件的行为,它不是简单的父子关系。我更推荐用状态字段 + 流转日志的组合。
我的做法是:状态只存当前节点,历史轨迹全部落到日志表。这样既方便查询当前进度,又能完整还原业务过程。这套方案在答辩时可以很自然地说:它一方面保证了流程的可追踪性,另一方面避免了状态字段的历史值覆盖问题。
4. 后端落地:状态机、鉴权、定时任务与文件管理
4.1 后端工程结构与分层规范
后端工程我按SpringBoot标准结构搭,启动类放在RealEstateApplication,Mapper接口统一加@Mapper注解或者在启动类上使用@MapperScan。配置文件用application.yml,数据库配置、端口、日志级别、文件上传路径都写在里面。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/real_estate?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: map-underscore-to-camel-case: true一个容易踩的坑是数据库连接串里的serverTimezone。不配这个东西,查询日期数据时会遇到时区偏移,看起来像是"少了8个小时",其实就是这没配好。我在联调时被这个问题卡过一下午,后来一查就是时区问题。
4.2 登记流程的状态机实现
登记流程我做了四个状态:草稿、受理、审核、登簿。后端接口层面,我按"动作"来划分,而不是按"状态"来划分。也就是说,我不提供"修改状态"这种危险接口,而是提供"提交受理"、"提交审核"、"审批同意"、"登簿"这类语义明确的接口。每个接口内部去校验当前状态是否合法。
我定义了一个状态枚举:
public enum RegisterStatus { DRAFT(0, "草稿"), ACCEPTED(1, "受理"), APPROVED(2, "审核通过"), REGISTERED(3, "登簿"), REJECTED(4, "驳回"); private final int code; private final String desc; // 构造、getter省略 }流转的时候,用同一个状态更新SQL做控制,核心逻辑是"当前状态必须等于预期状态,才允许更新到下一状态",防止并发场景下两个人同时操作同一单:
@Update("UPDATE register_bill SET status = #{newStatus}, current_handler = #{handler} " + "WHERE id = #{billId} AND status = #{oldStatus}") int updateStatus(@Param("billId") Long billId, @Param("oldStatus") int oldStatus, @Param("newStatus") int newStatus, @Param("handler") String handler);每次更新状态时,同时写入一条register_step_log。这样"状态机 + 日志表"就完整了。答辩时老师问"你这系统能不能支持审批驳回、退回某个环节",你可以直接展示日志表:每一次操作都有记录,天然支持回溯。
4.3 JWT登录拦截与角色权限
登录我用了JWT,理由没什么花哨:无状态、天然适合前后端分离。用户登录成功后,后端返回一个token,前端存到Pinia里,后续请求在Axios拦截器中加Authorization头。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && JwtUtils.verify(token)) { return true; } response.setStatus(401); return false; } }权限控制我不建议在毕设里引入Spring Security全家桶,学习成本高,配置复杂,容易把时间耗在过滤器链上。我采用的是自定义注解 + 拦截器校验角色的轻量方案:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String[] value(); }在需要权限的接口上加上这个注解,然后拦截器里取当前用户的角色去比对。这个方案的好处是:代码简单、可解释性强、也能体现你对权限模型的理解。
4.4 租赁到期提醒的定时任务
毕设里加定时任务是一个小亮点。我在租赁管理模块里做了一个"到期前30天自动提醒"的功能。SpringBoot实现这个太简单了,两步搞定。
启动类加@EnableScheduling,然后在定时任务类上写:
@Component public class LeaseReminderTask { @Resource private LeaseContractMapper leaseContractMapper; @Scheduled(cron = "0 0 8 * * ?") public void reminderLeaseExpire() { LocalDate date = LocalDate.now().plusDays(30); List<LeaseContract> expiringList = leaseContractMapper.selectExpiringBefore(date); for (LeaseContract contract : expiringList) { // 生成提醒记录或者调用消息推送 } } }定时任务一定要做"可观测",至少加个日志输出,否则你演示时都不知道它到底有没有跑。我一般会在任务执行到最后打印一行:任务执行完成,共扫描租赁合同xx条,其中到期预警xx条。这个日志也是答辩时能直接拿出来的干货。
4.5 附件上传与访问链路
房屋登记业务往往需要上传材料附件,比如身份证扫描件、房屋买卖合同、登记申请表。我用的方案是把文件保存到本地磁盘目录,然后通过SpringBoot的静态资源映射暴露访问URL:
file: upload-dir: D:/dev/real_estate/upload/@Configuration public class WebResourceConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + uploadDir); } }测试的时候有个细节要注意:很多同学写测试脚本时,试图通过RestTemplate直接传MultipartFile,结果总是报错。正确姿势是把文件包装成ByteArrayResource再塞进MultiValueMap。这个坑在对接联调时经常出现,提前知道能省不少时间。
5. 前端Vue落地:从路由守卫到搜索表格的组件化
5.1 搭建Vue 3项目的几个关键步骤
前端项目我用npm create vite@latest初始化,选择Vue模板,然后按需安装依赖。目录结构我习惯划分成:
src ├── api ├── assets ├── components ├── layout ├── router ├── store ├── views ├── utils └── App.vueapi目录里每个模块对应一组后端接口,比如api/register.js、api/estate.js。这样做的好处是页面组件中不会到处散落axios.get,维护时一眼就能找到某个业务的所有接口。
Vue项目初始化时最容易踩的坑是依赖安装失败。npm install报权限错误或者网络超时,先把registry切到国内镜像再试。另外就是版本冲突,建议锁定主要依赖的版本号,不要全用^前缀。
5.2 路由守卫与状态存储
路由我做了两个层级:外层是控制台Layout,内层是各业务页面。登录页独立在Layout之外。核心代码是路由守卫:
router.beforeEach((to, from, next) => { const token = useUserStore().token if (to.path === '/login') { next() } else if (!token) { next('/login') } else { next() } })这里有个细节:token放Pinia的同时,必须同步放一份到localStorage。因为Pinia是内存态,刷新页面就没了。刷新后重新从localStorage恢复用户信息,这是一个必坑点。
路由采用hash还是history模式也要提前想好。如果打算把前端打包后丢进SpringBoot的静态目录,建议直接用hash模式,省掉生产环境刷新404的问题。如果坚持用history模式,就得在后端或Nginx配合处理。
5.3 用插槽封装可复用的搜索表格页
不动产登记系统里,列表页非常多:权利人列表、房屋列表、登记列表、租赁列表。如果每个页面都从头写一遍搜索框和表格,项目会膨胀得很难看。我封装了一个SearchPro.vue,把搜索区域和表格主体抽象成可配置组件,核心是靠插槽做扩展。
<template> <div class="search-pro"> <el-form :model="queryParams" inline> <slot name="form-header" /> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <el-table :data="tableData" v-loading="loading"> <slot /> <el-table-column label="操作" align="center"> <template #default="scope"> <slot name="operations" :row="scope.row" /> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @current-change="fetchList" /> </div> </template>默认插槽放表格列,具名插槽operations放操作按钮。列表页的列千变万化,但外壳完全一样。这套组件化思路在答辩时值得重点讲,它能体现你理解"复用"这件事,而不仅仅是会调Element Plus的API。
5.4 ECharts图表与低代码化思路
首页统计看板我用了ECharts,做了两个图:一个是各登记类型的数量柱状图,一个是近期登记业务量趋势折线图。接口返回的数据结构不要让前端做太多二次加工,最好后端直接抛出一个适合ECharts的{ name, value }或者{ dates, counts }结构。
const res = await getRegisterStatistics() const option = { xAxis: { data: res.data.dates }, yAxis: {}, series: [{ type: 'bar', data: res.data.counts }] }如果时间富余,可以用el-dialog加一个拖拽组件,让用户在配置页里自由调整首页看板的展示卡片顺序。这不难做,但对"低代码化"这个词的呼应非常明显。答辩现场只要演示一次,效果就比你说的十句话都有用。
6. 联调、部署与演示:从开发机到答辩现场
6.1 vite代理解决本地联调跨域
本地开发时,前端跑在5173端口,后端跑在8080端口,浏览器的同源策略会拦掉请求。最快的解决方案是在Vite配置文件里加代理,注意这个方案只作用于开发环境。
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }代理设置好以后,前端请求/api/login会被转发到后端/login,跨域问题直接消失。开发环境和生产环境的接口基础路径不同,建议把baseURL拎成环境变量,不要硬编码。
6.2 生产构建的两种部署姿势
生产部署有两种常见姿势,两种都值得掌握。
第一种:把Vue打包放进SpringBoot,形成单一Jar包。
执行npm run build后,把生成的dist目录下的所有文件复制到src/main/resources/static,再重新打包SpringBoot。这样一条命令就能启动整个系统,非常适合答辩演示环境。缺点是前端代码更新后需要重新打包Jar。
第二种:Nginx托管前端,SpringBoot独立跑后端。
server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }第二种方式更接近真实的生产形态,也更能体现你理解运维部署。建议在毕设论文里把两种方式都写上,一个作为"开发演示方案",一个作为"生产部署方案"。
6.3 答辩演示与源码交付的注意点
答辩演示最怕的就是现场数据库是空的。我建议准备一套完整的演示数据,覆盖"刚录入的草稿单 -> 处于审核中的单 -> 已完成登簿的单 -> 一条即将到期的租赁合同"。演示时按业务顺序走:
- 第一步,登录系统,展示不同角色的看到菜单不同。
- 第二步,查询一条不动产单元信息,展示权籍字段。
- 第三步,发起一条转移登记,走完受理、审核、登簿流程。
- 第四步,展示流程日志。
- 第五步,打开首页看板,展示统计图表。
- 第六步,展示定时任务扫描到的租赁到期提醒。
另外,源码如果要发给老师或者队友,记得把配置文件里的数据库密码、本地路径替换成环境变量或者说明文件,避免别人拿到代码后因为绝对路径跑不起来。这种细节能在验收阶段帮你省很多沟通成本。
7. 我踩过的坑和答辩时的高频提问
7.1 五个高频踩坑复盘
第一个坑:日期序列化格式问题。后端LocalDateTime返回给前端,默认是ISO格式,而且Jackson如果没有正确配置,时区可能不对。在application.yml里加上统一格式转换:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8第二个坑:前端代理跑通了,生产环境却跨域。开发环境用Vite代理不需要后端配CORS,但前端构建成纯静态资源后,如果静态资源和API不在同源,跨域就会现形。建议后端统一配置CORS,或者在Nginx配置里处理,别只在开发环境测试跑一遍就完事。
第三个坑:MyBatis-Plus逻辑删除字段与唯一索引冲突。如果开启逻辑删除,而业务表上又建了唯一索引,逻辑删除掉的记录仍然占着唯一索引位置,再次插入同字段数据会冲突。演示时一旦遇到,要给老师解释清楚,或者把唯一索引改成联合唯一索引。
第四个坑:前端路由history模式刷新404。这个问题在部署部分反复出现。要么用hash模式,要么在Nginx里配try_files $uri $uri/ /index.html;。我建议毕设直接用hash模式,省心。
第五个坑:Vue路由参数丢失。列表页跳详情页时,如果有同学习惯把对象通过路由参数整个传过去,刷新后就会丢。正确做法是传ID,详情页再根据ID拉接口。
7.2 答辩时的四类高频提问与回应思路
"为什么选前后端分离架构?"
不要只说"主流"两个字。展开谈:后端独立提供RESTful接口,前端负责交互与渲染,两个团队可以并行开发;前端打包产物可交给CDN或Nginx托管,后端的负载能力可以独立扩展;在考试或实际项目中,同一套后端可以支撑Web端、管理后台端等多端复用。
"登记状态是怎么控制的?"
直接讲状态机的思路:用枚举定义合法状态,所有状态变更封装成语义化接口,更新时校验"旧状态是否符合预期",再执行到新状态的变更,同步写流程日志。强调这是对业务流程的建模,不是简单改一个字段。
"你的权限系统怎么实现的?"
谈JWT + 自定义注解 + 拦截器。说明为什么不用Spring Security:毕设项目角色模型简单,安全框架带来的过滤器链和配置复杂度超过收益。同时强调,理解方案能在复杂场景下替换为安全框架。
"定时任务如果单次执行时间超过周期,会不会重叠触发?"
这是一个很好的进阶问题,体现老师愿意深挖。SpringBoot的@Scheduled默认单线程执行,如果任务超时,下一次触发可能延后或者出现异常。解决方案是加分布式锁或者通过状态标记保证同一时间只有一次任务在执行。你只需要表达出"我理解这个风险",就已经超过大多数同学了。
7.3 如果时间富余,还可以加的点
如果核心功能都完成了还有精力,我建议按顺序加这几样东西,性价比从高到低排列:
- 操作日志模块:记录用户的所有关键操作。
- 数据看板下的API缓存:统计接口结果放Redis。
- 不动产单元变更历史:做一次比一次完整的"图谱式"展示。
- 批量导入:用EasyExcel导入Excel格式的权籍批量数据。
我指导的几届学生里,凡是把状态机、日志表、定时任务这几个点都做到了的,答辩基本都稳过。这个项目最终代码量不算大,但每个模块都能讲出设计逻辑,不是那种从网上下载就跑的"黑匣子"项目。对毕设来说,能在答辩时讲明白为什么,比写了一千行别人看不懂的代码重要得多。