这是一个典型的前后端分离项目,属于JAVA开发者练手、毕业设计或者小型CRM系统原型的高频题材。我不直接贴整份10万字的源码,而是以"架构师带徒弟"的口吻,把从表结构设计到前端联调、再到服务器部署的完整链路讲透,并给出核心代码的骨架和配置、踩坑点。这样既符合技术博客的阅读习惯,又是可以直接对着敲的实操笔记。
下面开始。
1. 这套系统到底在解决什么业务问题
房产销售系统听起来挺大,剥掉外壳,核心其实就是一条线索:楼盘 → 房源 → 客户 → 认购/签约 → 财务审核。
我之前在某家开发商的信息部门待过,最开始他们用的是Excel加微信群报备,客户撞单、折扣审批找不到人、房源状态更新不及时,各种幺蛾子。后来换了正儿八经的销售管理系统,但那是老牌C/S架构,装客户端费劲不说,营销总监在外地想看个实时去化率还得远程桌面。所以,前后端分离的B/S架构几乎是这类系统迭代的必然方向——浏览器打开就能用,手机也能看,权限一配就完事。
用SpringBoot + Vue + MyBatis + MySQL这个组合来写,不是因为它"网红",而是这个组合在中小型企业管理软件里确实能打:
- SpringBoot负责把后端服务的配置、依赖、部署大幅简化,一个jar包就能跑起来,内嵌Tomcat不用单独装容器;
- MyBatis是半自动ORM,复杂的多表关联查询、动态SQL非常好控制,尤其是房源筛选这种条件不固定的场景,比全自动的JPA更顺手;
- Vue做前端SPA(单页应用),配合Element UI这类组件库,开发后台管理界面的速度相当可观;
- MySQL承担业务数据存储,对于这种量级(几百套房源、几千个客户)绰绰有余,成本也低。
这套技术栈的典型适用人群有两类:一类是刚工作一两年的后端开发,想系统走一遍前后端分离项目的完整链路,打通"数据库设计 → 接口定义 → 前端联调 → 部署上线"的任督二脉;另一类是在校学生,需要一个业务逻辑完整、技术栈主流、能讲清楚设计思路的项目作为毕设或求职项目。这篇文章我会把技术选型的理由、数据库设计的思考过程、核心模块的代码骨架、以及部署时常见的坑都交代清楚,你可以把它当成一份可以直接照着动手的工程笔记。
你拿到这套源码之后,最忌讳的事情就是直接npm run dev和java -jar跑起来,然后对着页面说一句"哦,跑通了",那等于白拿。你要看的是:房源的去化状态是怎么流转的?折扣审批的流程状态是怎么设计的?客户跟进记录是怎么和主数据关联的?把这些业务逻辑捋清楚,这套代码才是你的。
先说清楚一个原则:业务系统的代码,难点从来不是某个算法,而是数据如何组织、状态如何流转、权限如何控制。我们后面所有的技术点,都是围绕这三件事展开的。
2. 从零设计数据库表:房产销售业务的骨架
2.1 核心表拆分逻辑:绝不做一张大表
很多初学者喜欢把房源、客户、成交记录全部塞到一两张表里,字段越加越多,最后一张表二三十个字段,查询慢不说,改起来更是噩梦。正常的做法是按业务域拆分,各表之间用外键逻辑关联(物理外键看团队习惯,我一般只用逻辑外键,避免插入更新时的约束开销)。
对于房产销售系统,最少需要这么几张核心表:
| 表名 | 用途 | 核心字段示例 |
|---|---|---|
building | 楼栋信息 | id, name, address, total_floors, start_time |
house | 房源信息 | id, building_id, unit_no, floor, area, total_price, status |
customer | 客户信息 | id, name, phone, id_card, level, source |
follow_record | 客户跟进记录 | id, customer_id, content, next_follow_time, operator_id |
sale_order | 认购/销售订单 | id, order_no, house_id, customer_id, total_amount, status |
sys_user | 系统用户 | id, username, password, real_name, role_id |
sys_role | 角色 | id, role_name, permission_code |
这里我要重点强调一下house表的status字段。
房源状态是整个系统的心脏,我见过很多失败的实现是把状态存成字符串("在售"、"已认购"、"已签约"),直接在代码里写死比较。正确做法是使用状态码(int或tinyint) + 数据字典解释:
-- 房源状态: 1-待售 2-锁定 3-已认购 4-已签约 5-已退款 status TINYINT NOT NULL DEFAULT 1 COMMENT '房源状态'锁定的含义要特别注意。客户看中了房子,但是还没交定金,这时候房源不能卖给其他人,所以要有一个"锁定"状态,并记录锁定时间和锁定的置业顾问。后面我会讲到这个状态如何在接口层流转,这里先记住:状态机设计是这类业务系统最核心的复杂度来源。
2.2 客户与房源的关联:跟进记录的设计
客户跟房产的关联不是简单外键,因为一个客户可能看过多套房子,一套房子也被多个客户看过。所以sale_order只是最终成交的关联,而看房过程、跟进历史需要一张独立的关联表。
CREATE TABLE `follow_record` ( `id` INT NOT NULL AUTO_INCREMENT, `customer_id` INT NOT NULL COMMENT '客户ID', `house_id` INT DEFAULT NULL COMMENT '本次跟进涉及的房源ID', `content` VARCHAR(500) NOT NULL COMMENT '跟进内容', `next_follow_time` DATETIME DEFAULT NULL COMMENT '下次跟进时间', `operator_id` INT NOT NULL COMMENT '跟进人', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户跟进记录';这个设计的巧妙之处在于:content是文本,house_id可有可无。这样既能记录"电话回访,客户说再考虑一下"这种纯沟通内容,也能记录"带看A栋1203,客户对采光满意,对价格有异议"这种关联房源的记录。
为什么不在sale_order里加一个follow_ids字段?因为关系型数据库不适合存数组,后续你要统计"这个客户被跟进了几次""哪个顾问跟进转化率最高",SQL都不好写。拆一张独立的表,所有问题迎刃而解。
2.3 用户权限:RBAC模型落地
销售系统一定要有权限控制,不然置业顾问能看见所有人的客户资料,那还得了。经典的RBAC(基于角色的访问控制)模型我直接拿来用:
- 用户表:只存基本信息,不直接存"是经理还是销售"
- 角色表:定义有哪些角色,比如
SALES、MANAGER、ADMIN - 用户-角色关联(或是直接在用户表加role_id,小型系统够用)
我这里选了最简化的方案:sys_user表加一个role_id字段,不单独拆用户角色关联表。理由是小规模系统角色相对固定,没必要为了理论上的扩展性增加复杂度。
public class LoginUser { private Integer userId; private String username; private String realName; private String roleCode; // SALES / MANAGER / ADMIN }接口鉴权我用的Filter + 注解方式,没有引入Spring Security。不是Spring Security不好,而是对这个体量的系统来说它太重了——一大堆过滤器链和默认配置,初学者很容易配着配着就晕了,出了问题都不知道是自己配置错了还是框架的行为。用一个自定义拦截器,几十行代码就能实现"登录才能访问 + 特定角色才能操作"的效果,逻辑完全在自己掌控之中。
3. 后端接口与状态流转:用SpringBoot把业务写清楚
3.1 登录与鉴权:JWT的无状态设计
前后端分离之后,Session这套东西就别用了。原因很简单:前端和后端不在同一个域名/端口下,Cookie的跨域处理非常麻烦,而且如果以后要拆微服务,Session共享又是一堆事。目前的主流做法是JWT(JSON Web Token)。
JWT的本质,就是把用户信息加密后生成一串token,服务器不存储会话状态,客户端每次请求把token带上,服务器验签通过就放行。
// JwtUtil.java 核心方法 public static String generateToken(Integer userId, String username, String roleCode) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", roleCode) .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)) // 12小时过期 .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); }这里有几个细节你需要特别留意,都是我实际踩过的坑:
- SECRET密钥不要太短,HS256要求至少256位,否则某些版本的jjwt会直接报错。建议用32位以上的随机字符串;
- 过期时间要有策略,12小时对销售系统合适,太短了用户频繁登录,太长了有安全风险。如果你要"记住我"功能,可以再发一个refresh_token,但这里先不做;
- 登录接口返回token之外,还要返回用户信息和角色信息,前端要根据角色渲染不同的菜单和按钮。比如普通销售看不到"折扣审批"这个菜单。
自定义拦截器的注册方式注意一下,新版SpringBoot用WebMvcConfigurer:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/register"); } }不要图省事只注册拦截器而不加excludePathPatterns,否则你的登录接口也会被自己拦截,这是新手最常见的配置错误。
3.2 房源上架与状态流转:状态机的实现逻辑
房源管理是整个系统的数据源头。销售录入房源、管理员审核上架、客户锁定、成交下架,这个流程的状态必须严密控制。
我设计的house表状态流转如下:
- 1-待售:房源已录入,可被搜索和展示
- 2-锁定:客户表达意向,销售在系统里点击"锁定",其他人不可见/不可操作
- 3-已认购:客户交定金,签订认购书,状态不可逆(除非退款)
- 4-已签约:签订正式购房合同
- 5-已退款:认购或签约后发生退订
对应的后端Service层代码逻辑是:
// 核心的锁定操作 @Transactional public Result lockHouse(LockHouseDTO dto) { House house = houseMapper.selectById(dto.getHouseId()); // 防并发:通过update语句的条件判断状态,而不是先查后改 int rows = houseMapper.compareAndSetStatus( dto.getHouseId(), 1, // 期望当前状态为待售 2, // 目标状态为锁定 dto.getCustomerId(), dto.getOperatorId() ); if (rows == 0) { return Result.error("房源已被锁定或售出,请刷新后再试"); } // 写跟进记录 followRecordMapper.insert(...); return Result.success(); }这里的关键是compareAndSetStatus这个方法。
这背后是一个经典的并发控制问题。两个销售同时点击锁同一套房,如果代码是"先select,判断status等于1,再update set status=2",那在高并发下可能出现都通过了if判断、都执行update的情况——最后这套房被两个人同时锁定,数据就错了。
正确的做法是把状态判断放到SQL语句里,让数据库帮我们做原子操作:
UPDATE house SET status = #{targetStatus}, lock_user_id = #{operatorId} WHERE id = #{houseId} AND status = #{expectStatus}update语句自带行锁,判断条件和更新在同一操作里完成,就不会出现并发问题了。这个compareAndSet的写法,比你写十行同步代码都管用,也是面试的时候能讲出来的亮点。
3.3 订单与折扣审批:多级状态联动
订单模块牵扯的东西最多,因为一张订单要关联房源、客户、价格、折扣、审批状态等多个维度的信息。
我设计了两张表:sale_order(订单主表)和order_approval(审批记录表)。这里解释一下为什么要拆:
- 订单主表只存业务核心数据:订单编号、房源、客户、认购金额、折扣金额、最终成交价、下单时间、状态;
- 审批记录表存审批流:谁提交的、提交时间、当前审批人、审批结果、审批意见、审批时间。
一个订单可以有多次审批记录,一对多的关系,天然适合拆表。订单状态我设计了1-待审批 2-审批通过 3-审批驳回 4-已签约 5-已退款。
销售提交认购后,订单进入"待审批"状态,管理员或项目经理登录后可以看到待审批列表,点"通过"或"驳回",同时写入审批记录。这里我用了一个简单的时间线设计:
public List<OrderApprovalVO> getApprovalHistory(Integer orderId) { return approvalMapper.selectByOrderId(orderId); }前端用时间线组件渲染这条记录列表,销售可以看到自己的单子当前审批到哪一步、被谁处理过、意见是什么。这种"状态 + 记录"的双表设计,几乎可以套用到所有带审批的业务系统(请假、报销、采购……)。
3.4 客户管理:撞单判断怎么做
客户管理模块有一个业务细节很容易被忽略,那就是撞单判断。
两个销售录入同一个手机号的客户,系统要不要提示?如果完全不处理,就会出现两个销售抢同一个客户的情况,最后客户买了房,业绩算谁的?这是一线销售管理系统的经典问题。
我的处理方案比较务实:
- 客户表中手机号建立唯一索引,作为强校验;
- 新增客户时如果手机号已存在,则返回该客户是否关联了其他销售;
- 如果是"公海客户"(未分配销售),当前销售可以直接认领;
- 如果已分配给其他销售,则提示"该客户已被跟进,如需协作请联系管理员"。
公海客户这个概念顺带说一下:那些录入系统但没有分配销售顾问的客户,放在公共池子里谁都可以联系,联系后点击"认领",该客户就归你了。这是销售系统里很常见的设计,能有效避免客户资源浪费。
@Transactional public Result addCustomer(CustomerDTO dto, LoginUser loginUser) { Customer exist = customerMapper.selectByPhone(dto.getPhone()); if (exist != null) { if (exist.getOwnerId() == null) { customerMapper.updateOwner(exist.getId(), loginUser.getUserId()); return Result.success("认领成功"); } return Result.error("该手机号已被" + exist.getOwnerName() + "跟进"); } // 新增客户逻辑 }4. 前端Vue的部分:页面结构、权限控制和接口封装
后端接口写得再漂亮,前端展示不出来也白搭。Vue + Element UI这套组合,核心思路可以总结成三个词:组件化、状态管理、路由守卫。
4.1 项目目录结构规划
frontend/ ├── public/ ├── src/ │ ├── api/ # 接口请求封装 │ │ ├── request.js # axios实例封装 │ │ ├── house.js # 房源模块接口 │ │ ├── customer.js # 客户模块接口 │ │ └── order.js # 订单模块接口 │ ├── router/ # 路由配置 │ │ └── index.js │ ├── store/ # Vuex状态管理 │ │ ├── index.js │ │ └── modules/user.js │ ├── views/ # 页面组件 │ │ ├── login.vue │ │ ├── dashboard.vue │ │ ├── house/ │ │ │ ├── list.vue │ │ │ └── detail.vue │ │ ├── customer/ │ │ │ └── list.vue │ │ ├── order/ │ │ │ ├── list.vue │ │ │ └── approval.vue │ │ └── system/ │ │ └── user.vue │ └── main.js按业务模块划分目录,而不是按"页面/组件/工具"这种技术类型划分,这样后期维护时找文件成本最低。这是一个工程化上的小经验。
4.2 axios封装:请求拦截和错误处理
前端和后端交互,最大的痛点就是token怎么带、出错怎么提示、登录过期怎么处理。不封装axios的话,每个页面都要写一堆重复代码。
// api/request.js import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: '/api', // 通过Vite代理转发,避免跨域 timeout: 15000 }) // 请求拦截器:自动携带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error)) // 响应拦截器:统一处理错误 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) } )看到重点没?baseURL: '/api',不是完整的后端地址。这是因为开发和部署都存在跨域问题,最省事的方案是用前端开发服务器的代理转发。
Vite的配置:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', // 后端服务地址 changeOrigin: true } } } })为什么要这样?如果用完整地址http://localhost:8080/api/xxx,浏览器直接请求后端,一定会触发跨域限制。而通过代理转发,浏览器请求的是前端自己的服务器(比如http://localhost:5173/api/xxx),不跨域,由前端服务器转发到后端。把跨域问题从"浏览器层面"转移到"服务器层面",这是前后端分离项目最优雅的解决方案。
4.3 路由守卫:前端页面权限控制
后端接口有拦截器验证权限,前端也要有对应的页面权限控制,否则没权限的用户虽然请求不到数据,但能看到页面框架和菜单,体验很差。
实现方式是用Vue Router的beforeEach导航守卫:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } next() })这是最基础的登录拦截。再进一步,根据角色动态生成菜单:
function getMenuByRole(roleCode) { const allMenus = [ { path: '/dashboard', label: '工作台', roles: ['SALES', 'MANAGER', 'ADMIN'] }, { path: '/house', label: '房源管理', roles: ['SALES', 'MANAGER', 'ADMIN'] }, { path: '/customer', label: '客户管理', roles: ['SALES', 'MANAGER', 'ADMIN'] }, { path: '/order', label: '订单管理', roles: ['SALES', 'MANAGER', 'ADMIN'] }, { path: '/approval', label: '折扣审批', roles: ['MANAGER', 'ADMIN'] }, { path: '/system', label: '系统设置', roles: ['ADMIN'] } ] return allMenus.filter(m => m.roles.includes(roleCode)) }这里要注意:前端的权限控制只是提升用户体验,真正的安全防线永远在后端。有些初学者只做了前端菜单隐藏,结果懂技术的人绕过前端直接调用后端接口,数据照样泄露。所以前端的权限控制必须配上后端的接口拦截,两者缺一不可。
4.4 房源筛选页面的实现思路
房产系统的房源列表页,通常需要支持多条件组合筛选:楼盘、楼栋、朝向、面积区间、总价区间、房源状态。这种筛选条件不固定的场景,后端用MyBatis动态SQL,前端用Element UI的Form表单。
前端部分不复杂,就是几个输入框和下拉框,关键是把筛选参数组织好传给后端:
const queryParams = ref({ buildingId: '', minArea: '', maxArea: '', minPrice: '', maxPrice: '', status: '' }) function search() { // 把空字符串字段删除,避免后端收到无意义的参数 const params = {} Object.keys(queryParams.value).forEach(key => { const val = queryParams.value[key] if (val !== '' && val !== null) { params[key] = val } }) loadHouseList(params) }然后是MyBatis的动态SQL,这是MyBatis最核心的价值所在。注意我用的是<where>标签自动处理AND拼接的逻辑:
<select id="selectByCondition" resultType="com.demo.pojo.HouseVO"> SELECT h.*, b.name AS building_name FROM house h LEFT JOIN building b ON h.building_id = b.id <where> <if test="buildingId != null and buildingId != ''"> AND h.building_id = #{buildingId} </if> <if test="minArea != null and minArea != ''"> AND h.area >= #{minArea} </if> <if test="maxArea != null and maxArea != ''"> AND h.area <= #{maxArea} </if> <if test="minPrice != null and minPrice != ''"> AND h.total_price >= #{minPrice} </if> <if test="maxPrice != null and maxPrice != ''"> AND h.total_price <= #{maxPrice} </if> <if test="status != null and status != ''"> AND h.status = #{status} </if> </where> ORDER BY h.id DESC </select>这里有一个新手常犯的错误:在XML中写<或>符号会报错。XML里的小于号需要转义为<,大于号需要转义为>。我第一次写的时候忘了转义,服务启动直接报XML解析错误,排查了半天才反应过来。
分页也不要用PageHelper插件,虽然它好用,但对初学者来说,理解LIMIT #{offset}, #{pageSize}的原理更重要,而且在小型系统里自己写分页完全够用。LIMIT后面第一个参数是偏移量,第二个是每页条数,前端传pageNum和pageSize,后端计算offset = (pageNum - 1) * pageSize,就这么简单。
5. 从本地到服务器:部署过程中的槽点与排查路径
代码写完了,本地也跑通了,接下来是部署上线。很多人在这一步翻车,因为本地环境和你租的云服务器环境总有各种细微差别,这些差别平时不显眼,部署的时候就全冒出来了。
5.1 后端部署:Jar包的运行参数
SpringBoot用Maven打出来的jar包是自带Tomcat的,部署起来很简单,但有几个问题必须处理:
端口占用:默认8080可能被占,或者你想用其他端口,启动时加参数:
java -jar house-sale-0.0.1.jar --server.port=8081数据库连接:开发环境的数据库连接写的是localhost:3306,服务器上的MySQL要么端口不同、要么密码不同,部署时建议用外部配置文件覆盖jar包内的配置:
java -jar house-sale-0.0.1.jar --spring.config.location=/etc/house-sale/application.yml这样不用重新打包,直接改服务器上的配置文件就能换数据库地址,非常灵活。
后台运行:直接java -jar的话,关闭终端进程就死了,要用nohup:
nohup java -jar house-sale-0.0.1.jar --spring.profiles.active=prod > /var/log/house-sale.log 2>&1 &2>&1表示把错误输出也重定向到同一个日志文件,不然你排查错误的时候发现日志里啥都没有,全打到控制台去了。
5.2 前端部署:build之后是静态文件
前端做完开发,要发布的是构建后的静态文件,不是源码。执行:
npm run build会在dist目录下生成一堆静态文件(HTML、CSS、JS),这些文件要放到Nginx的html目录(或者你自己指定一个路径)。
Nginx配置的核心有两块:root指定静态文件路径、location /api把那边的接口请求转发给后端服务:
server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /var/www/house-sale-front; index index.html; try_files $uri $uri/ /index.html; # 关键!刷新页面时路由不会404 } # 后端接口转发 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;这一行是无数前端项目的救命稻草。Vue Router如果使用history模式,用户在浏览器直接刷新/order/approval这个路径时,Nginx会先去查找对应的静态文件,找不到就返回404。而try_files的意义在于:找不到文件时,把请求回退到index.html,由Vue Router接管路由解析。如果不加这行,刷新页面就是白屏加404,用户直接懵了。
5.3 数据库初始化与数据迁移
部署时数据库这块也容易被忽略。开发时你可能是用Navicat手工建的表,到了服务器上要执行一遍建表SQL。我的建议是:把建表语句整理成一个init.sql,连同初始数据(管理员账号、角色数据、基础字典)一起在服务器上执行。
项目里通常会提供这个SQL文件,你只需要:
mysql -u root -p < /www/house-sale/init.sql执行完检查一遍表和数据:
USE house_sale; SHOW TABLES; SELECT * FROM sys_user;有些版本会把sys_user表的管理员密码做了BCrypt加密,初始密码是123456,被加密成了很长一串字符串。这个初始化的密码别搞丢了,否则第一次登录都登不进去。有些项目的init.sql里还自带了演示数据(几个楼盘、几十套房源、几个客户),这些数据在开发和调试接口时非常有用,但生产上线前建议清掉。
5.4 我排查过的一个经典问题:前端404但接口通
有一次部署完,输入域名,页面能打开(说明Nginx配置对了),登录也成功了(说明接口转发通了),但跳转到工作台之后,刷新一下,页面直接404。
这个问题的排查路径非常典型:
- 首先怀疑Nginx的
try_files,检查之后发现配了,不是这个问题; - 然后我打开浏览器开发者工具,切换到Network面板,发现刷新时请求的URL是
/dashboard,返回HTML是404页面的内容; - 再检查Nginx配置,发现问题:我把
try_files写到了location /里面,但history路由对应的路径请求并没有命中location /,而是命中了别的location规则; - 最后解决方案是调整location的匹配优先级,把
try_files放到更通用的location块中,并添加location / { ... }兜底。
这个问题的根源是对Nginx的location匹配规则不够熟悉:location /是前缀匹配,对所有路径生效,而其他更具体的location会覆盖它。我后来总结的经验是:前端的history模式部署,一定要把try_files写在最外层location中,不然刷新404的坑会反复出现。
6. 这套源码我建议你怎么去动它
源码拿到手,别急着跑,我建议你按照下面的顺序去研究,这样才能最大化吸收价值:
第一步:看数据库设计。用Navicat或命令行连上MySQL,把每张表的字段、索引、注释过一遍。对照我前面讲的状态流设计,把"房源1-5个状态"和"订单1-5个状态"在纸上画出来,想清楚什么操作触发什么状态变更。
第二步:看后端接口文档。项目里一般有Swagger(接口文档),或者从一开始的Controller方法上面就有完整的@ApiOperation注解。按模块一个个过:登录接口怎么验证密码、房源接口怎么处理状态流转、订单接口怎么产生编号。重点看一下@Transactional用在哪些方法上,想明白为什么这些地方需要事务。
第三步:跑通前后端联调。前端npm install、npm run dev,后端启动SpringBoot,浏览器打开开发服务器地址,走一遍完整的业务流程:登录 → 添加客户 → 添加房源 → 客户锁定房源 → 提交订单 → 审批通过 → 签约。在联调的过程中,F12打开开发者工具,Network面板里看每个请求的URL、参数、响应,体会前后端数据是怎么流动的。
第四步:试着改代码。这是检验是否真正理解的关键一步。比如:
- 给房源表加一个"户型"字段,从数据库到后端接口到前端表单,全链路加上;
- 把折扣审批改成两级审批(销售经理审批 → 总监审批),订单状态从"待审批"改为"待销售经理审批/待总监审批";
- 加一个统计报表接口,按楼盘维度统计去化率(已售套数/总套数)。
以上每一步我都很建议你真的做一遍。改代码的过程比看代码学到的东西多十倍,因为你不仅要理解原有的设计逻辑,还要自己设计数据结构、接口、页面,碰到问题还得自己排查。我见过一个A同学,他拿到源码之后,前后花了三天把房源多了个"赠送车位"的布尔字段,整个链路全改通了,用这段经历直接在校招面试里讲了一个完整的"技术方案设计与落地"的故事。
如果你完全没接触过Vue或者SpringBoot,建议先把基础语法过一遍,不用特别深,能看懂接口调用和组件数据绑定的程度就行。
这套房产销售系统的价值,不在于它用了多前沿的技术,而在于它是一个业务逻辑闭环的完整工程项目——从前端的页面交互,到后端的接口与事务,再到数据库的表结构设计,你都能看到真实的业务系统是怎么组织和串联的。把它彻底吃透,你就掌握了独立开发一个小型业务系统的全部技能。