1. 写在开发之前——这个系统到底要解决什么问题
1.1 房地产销售管理,管的核心不是"卖房",而是"信息流"
很多同学一看到"房地产销售管理系统"这个题目,第一反应是做一个房源展示网站:放几张户型图、写几个楼盘介绍,再弄个在线咨询按钮就完事了。我最初也是这么想的,直到真正接触了业务需求才意识到,房地产销售管理的核心其实不是"展示房子",而是管理从获客到成交的完整信息流。
一个销售团队在日常工作中要处理的信息量是非常惊人的:每个顾问手里同时跟进的客户可能有二三十个,每个客户处在不同的阶段——有的刚来咨询、有的已经看过几次房、有的正在谈价格、有的已经交完定金等着签约。如果没有一个系统把这些状态统一管起来,全靠Excel和微信聊天记录,信息散落是必然的,客户跟到一半忘了上次谈到什么价格,顾问离职把客户资源整个带走,这些情况在真实门店里太常见了。
这套系统的价值就在这儿:把房源、客户、带看记录、合同、回款这些分散的数据集中管理起来,让每个角色的工作状态都透明可见。开发之前先把这些业务场景想透,比急着写代码重要得多。
1.2 从业务场景反推功能模块,而不是从技术反推
做毕业设计或者课程设计的时候,有个很容易走偏的思路:先想"我会什么技术",再想"这些技术能做什么功能"。正确顺序应该反过来——先把业务场景里需要做的事列出来,再看用什么技术实现最合适。
我梳理下来,一个完整的房地产销售管理系统,业务上必须覆盖这几个环节:
房源管理:房子从哪里来、状态是什么。包括楼栋、单元、户型、面积、朝向、单价、总价,以及最关键的状态字段——在售、预留、已预定、已签约、已售。这里的预留状态很容易被忽略,但它对应的是真实的业务场景:销售带着客户看中某套房,客户需要回家跟家人商量,销售会先帮客户"锁房"一段时间,防止别的客户抢走。
客户管理:客户是谁、需求是什么、跟到哪一步了。这里要管的不只是客户姓名电话,更重要的是意向等级和跟进记录。一个客户第一次到访可能是闲逛,但看完样板间之后意向变成了高,这时候他是不是需要再次跟进?谁跟进的?上次聊了什么?这些信息都必须在系统里留痕。
带看与跟进管理:销售带客户去了哪套房、客户什么反应、下次约什么时间。这是销售管理系统的核心动作,也是判断一个顾问工作状态的重要依据。
合同与交易管理:谈成了之后,从认筹金到定金、首付、尾款,每一步都有单据和金额。合同编号需要自动生成,金额需要跟房源总价对上,成交之后房源状态要自动变为已售。
统计报表:管理层关心的是本月成交几套、回款多少、哪个顾问业绩最好、哪个楼盘卖得慢。这个功能直接决定系统是给基层操作人员用的,还是给管理层用的。
这五个环节串起来,就是一条完整的销售业务链。系统的功能边界也就清楚了:不是做一个房源展示网站,而是一个销售过程管理工具。
1.3 角色设计:三类人用这个系统,需求各不相同
需求分析阶段最容易漏掉的就是角色视角。同样一个房源列表,不同角色看到的和能用到的完全不一样。
我把系统角色分成三类:管理员、销售顾问、客户。
- 管理员:看全量数据,管所有的房源录入、人员分配、合同审核。他的核心诉求是掌控和统计。
- 销售顾问:只看自己名下或所属门店的客户和房源,核心诉求是"方便"——快速查到自己客户的跟进状态、快速登记带看记录。这是系统使用频率最高的角色,操作的便捷性直接影响他们愿不愿意用这个系统。
- 客户:客户能用的功能相对少,一般是查看在售房源信息、在线预约看房、查看自己名下合同的进度。注意,客户侧的登录方式和内部员工的登录方式建议分开处理,客户端用手机号验证码登录,员工端用账号密码。
这三种角色的存在,直接决定了系统必须要有权限控制,而且必须在后端做好校验,不能光靠前端隐藏按钮。
2. 技术选型与架构设计——为什么是Spring Boot,以及怎么设计数据库
2.1 技术栈选型的考量:Spring Boot是起点,不是全部
标题里点名了基于Java和Spring Boot,这个选择没什么争议。Spring Boot的优势在这个项目里体现得很明显:自动装配让开局配置量大幅减少,内嵌Tomcat让部署变得极其简单,Spring生态里现成的方案几乎覆盖了所有需求——MyBatis-Plus做持久层、Spring Security做权限、Redis做缓存、JWT做无状态认证,这些都是经过大量生产环境验证的成熟组合。
版本选择上我要多说一句:不要一上来就追最新版。Spring Boot的版本选择有一个很现实的逻辑——你要用的中间件版本它是否兼容。比如我用的是Spring Boot 2.7.18,这个版本非常关键,因为它是一个长期维护的LTS版本,同时对Java 8的支持非常完善。很多高校机房和实训环境还在用JDK 8,如果选了Spring Boot 3.x,它强制要求JDK 17,你的开发环境可能直接卡死。
我就见过有人选Spring Boot 3.2,然后发现本地的MySQL驱动、Redis客户端都要跟着升版本,别的同学用的老教程里的配置根本跑不起来,最后又在踩版本兼容性的坑。建议:这种规模的项目选Spring Boot 2.7.x,搭配JDK 8,稳妥省心。
架构上我采用的是前后端分离方案:后端纯RESTful API,前端用Vue + Element UI(如果只是需要快速出效果,也可以用Thymeleaf做服务端渲染,看你自己擅长什么。不过前后端分离在后期扩展和分工上会更灵活,也符合当前企业的技术栈习惯)。
核心结构如下:
springboot-realestate ├── src/main/java/com/project/estate │ ├── controller // 接口层 │ ├── service // 业务层 │ ├── mapper // 数据访问层(MyBatis-Plus) │ ├── entity // 实体类 │ ├── dto // 数据传输对象(避免直接暴露实体) │ ├── config // 配置类(跨域、拦截器、安全配置) │ ├── utils // 工具类(JWT、时间处理、统一返回) │ └── EstateApplication.java ├── src/main/resources │ ├── mapper/xml // 复杂SQL写在这里 │ └── application.yml └── frontend // Vue项目2.2 数据库设计:五张核心表,关系理清一切清晰
数据库设计是这个项目最重要的环节。表设计不合理,后面写业务代码会处处别扭。我整理了核心表结构,先看整体关系,再逐个解释为什么要这么设计。
规划的核心表有6张:用户表(sys_user)、角色表(sys_role)、房源表(house_info)、客户表(customer_info)、带看记录表(visit_record)、合同表(contract_info)。另外需要一张用户-角色关联表(sys_user_role),如果是多角色权限系统,还需要权限表(sys_permission)和角色-权限关联表(sys_role_permission)。
房源表 house_info的关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| house_code | varchar | 房源编号(如 XQ001-1-201) |
| building_name | varchar | 楼栋/小区名称 |
| unit_no | varchar | 单元号 |
| room_no | varchar | 房号 |
| house_type | varchar | 户型(如 3室2厅) |
| area | decimal(10,2) | 建筑面积(平方米) |
| orientation | varchar | 朝向(南/北/东南等) |
| floor | int | 所在楼层 |
| total_price | decimal(12,2) | 总价(万元) |
| unit_price | decimal(12,2) | 单价(元/平方米) |
| status | tinyint | 0-在售 1-预留 2-已预定 3-已售 4-下架 |
| decoration | varchar | 装修情况 |
| image_url | varchar | 房源图片地址 |
| remark | varchar | 备注 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
这个表要特别注意状态字段要设置预留(预留)状态。很多初学者只设在售和已售两种状态,但真实业务里,"客户看中了、但还没交定金"这个中间状态非常常见。没有预留状态,销售就只能通过口头约定来留房,很容易引发纠纷。
客户表 customer_info的关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| customer_name | varchar | 客户姓名 |
| phone | varchar | 手机号 |
| id_card | varchar | 身份证号 |
| intent_level | tinyint | 意向等级 1-低 2-中 3-高 |
| budget_range | varchar | 预算范围 |
| requirement | varchar | 需求描述 |
| source | varchar | 客户来源(网络/转介绍/门店/电话) |
| owner_id | bigint | 归属销售顾问ID |
| status | tinyint | 0-潜在 1-跟进中 2-已成交 3-已流失 |
| create_time | datetime | 创建时间 |
客户表这里我要重点提醒:客户资源归属字段 owner_id 必须要有。这个字段关系到销售顾问的业绩归属,也是权限过滤的基础——销售顾问登录系统后,只能看到 owner_id 等于自己ID的客户,这是系统的基本规则。
带看记录表 visit_record:记录销售顾问带客户看了哪套房、客户反馈如何、下次跟进时间是什么时候。每个顾问的日常工作留痕都在这里,管理人员可以通过这个表统计顾问的工作量。
合同表 contract_info:关联客户ID和房源ID,记录合同编号、成交金额、首付金额、签约时间、审核状态。合同金额必须和房源总价关联校验,这条业务规则在后面代码里要重点保障。
这五张表之间的关系可以理成一条业务链:
销售顾问(sys_user)→ 认领客户(customer_info)→ 安排带看(visit_record 关联 house_info)→ 成交签约(contract_info 关联 customer_info 和 house_info)
顺着这条链做设计,代码写起来逻辑非常顺,也不会出现"找不到字段存某个信息"这种尴尬情况。
2.3 配置文件的边界和规范
application.yml是项目的枢纽配置。我的习惯是把环境相关的内容全部集中在这里,方便在不同环境之间切换。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/estate_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key-change-me expire-hours: 24几个容易踩坑的地方:数据库连接串里的useUnicode=true&characterEncoding=utf8一定要加,否则中文乱码跑不掉;serverTimezone=Asia/Shanghai也要配,MySQL 8.x 不指定时区会报错;MyBatis-Plus 的逻辑删除配置可以把deleted字段管理起来,避免每次都手动追加条件。
3. 核心功能模块的实现——从房源到成交的业务闭环
3.1 房源管理模块:状态流转是设计的核心难点
房源模块的业务逻辑是管理房源全生命周期。最核心的部分是房源状态的流转逻辑,我把它设计成一个状态机:
0-在售:可被客户查看,可被销售选择,可设置预留1-预留:被某个客户锁定,其他人不能查看或选择,限时保留2-已预定:客户已交定金,房源不再公开销售3-已售:签约完成,房源永久下架4-下架:主动下架,比如样板间不出售或房源存在纠纷
状态流转的操作代码如下(简化版):
@Override @Transactional(rollbackFor = Exception.class) public void changeHouseStatus(Long houseId, Integer targetStatus, Long operatorId) { HouseInfo house = houseMapper.selectById(houseId); if (house == null) { throw new BusinessException("房源不存在"); } // 校验状态流转合法性:已售/下架不能再被修改 if (house.getStatus() == 3 || house.getStatus() == 4) { throw new BusinessException("该房源已终止流转,无法变更状态"); } // 预留/预定时必须关联客户 if ((targetStatus == 1 || targetStatus == 2) && operatorId == null) { throw new BusinessException("预留或预定的房源必须绑定客户"); } // 更新房源状态 house.setStatus(targetStatus); house.setUpdateTime(LocalDateTime.now()); houseMapper.updateById(house); }这里要强调的是@Transactional注解,业务操作必须满足原子性——要么成功,要么回滚,不能出现中间态。
3.2 客户管理与跟进留痕:权限过滤不能只做前端
客户管理模块最常见的错误是只写增删改查,把最重要的"跟进记录"漏掉。跟进记录的本质是留痕——销售顾问每次给客户打电话、发消息、带看,都需要在系统里记一笔。没有这个功能,管理者就完全无法判断一个销售顾问的工作状态。
我在客户模块里加了三个接口:新增客户、分页查询客户(带权限过滤)、新增跟进记录。查询客户的分页代码核心逻辑如下:
@Override public PageResult<CustomerVO> pageCustomers(CustomerQuery query) { Page<CustomerInfo> page = new Page<>(query.getPageNum(), query.getPageSize()); // 关键逻辑:非管理员只能查自己名下的客户,防止越权 LambdaQueryWrapper<CustomerInfo> wrapper = new LambdaQueryWrapper<>(); if (!isAdmin(query.getOperatorRole())) { wrapper.eq(CustomerInfo::getOwnerId, query.getOperatorId()); } if (StringUtils.hasText(query.getKeyword())) { String keyword = query.getKeyword(); wrapper.and(w -> w.like(CustomerInfo::getCustomerName, keyword) .or().like(CustomerInfo::getPhone, keyword)); } IPage<CustomerInfo> result = customerMapper.selectPage(page, wrapper); // 实体转VO并返回 return PageResult.of(result); }权限判断放在 Service 层而不是 Controller 层,这是关键原则——Controller 只管参数接收和返回,真正决定"谁能看什么数据"的规则必须下沉到业务层,防止有人绕过接口直接调用。
3.3 合同模块与数据一致性:金额校验和防并发
合同管理是这个系统中数据一致性要求最高的模块。成交金额、首付款金额、贷款金额、房源状态、客户状态,这五个信息必须保持一致,任何一环出错,后续财务数据就是错的。
为了避免并发问题,我给合同创建逻辑加了两把锁:数据库乐观锁和Redis 分布式锁。
先看数据库层,在house_info表加一个version字段,每次更新房源状态时检查版本号:
UPDATE house_info SET status = 3, version = version + 1 WHERE id = #{houseId} AND status IN (0, 1, 2) -- 只允许从有效状态流转到已售 AND version = #{oldVersion}如果影响行数为 0,说明房源已经被其他人变更,直接抛出"房源状态已变更,请刷新后重试"。
再看看 Redis 锁,防止同一套合同被并发提交两份:
// 分布式锁:key = 房源ID,防止两个销售同时录入同一套房子的合同 boolean locked = redisLock.tryLock("LOCK_HOUSE_" + houseId, 30, TimeUnit.SECONDS); if (!locked) { throw new BusinessException("该房源正在处理中,请勿重复操作"); } try { // 创建合同、更新房源状态、更新客户状态…… } finally { redisLock.unlock("LOCK_HOUSE_" + houseId); }这两层保护就是实际项目里常说的"乐观锁 + 分布式锁"的组合方案,能覆盖绝大多数并发场景。
3.4 统计报表:管理者视角的核心功能
统计模块用的是 Dashboard 的形式。报表里的所有数据都必须是实时从业务表聚合出来的,不能存冗余的汇总表——数据实时性对管理层很重要,而且项目的并发量远没到需要预聚合的程度。
后端聚合接口我提供了几个核心维度:
- 按状态统计房源数量:在售多少、已预定多少、已售多少
- 按月份统计成交合同数及成交总金额
- 按销售顾问统计本月业绩排行
- 按户型统计销量分布
实现方式上,复杂一点的直接写SQL查询,比如按月统计成交数据:
@Select("SELECT DATE_FORMAT(sign_time, '%Y-%m') AS month, " + "COUNT(*) AS contractCount, SUM(deal_amount) AS totalAmount " + "FROM contract_info " + "WHERE sign_time >= #{startTime} " + "GROUP BY DATE_FORMAT(sign_time, '%Y-%m') " + "ORDER BY month DESC") List<MonthStatsVO> countMonthlyContracts(@Param("startTime") String startTime);报表数据量小,SQL聚合就够了,不需要引入额外的大数据组件。这个规模的项目,加一堆重型技术栈纯属自找麻烦。
4. 安全控制与常见隐患处理——XSS、恶意文件上传和权限绕过
4.1 登录认证与权限控制:JWT 比 Session 更适合前后端分离
前后端分离架构下,Session 方案需要额外处理跨域 Cookie 问题,JWT 方案更自然。我这里的做法是:登录成功后生成 JWT 返回给前端,前端每次请求带上Authorization头,后端拦截器校验 token,把用户ID和角色塞进请求上下文。
JWT 工具类的核心逻辑:
public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireHours * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); }拦截器里需要注意几点:放行登录、注册、房源展示等公开接口;校验 token 时区分token过期和token无效,给前端明确的响应用户重新登录;设置必要的请求头信息供后续业务使用。
4.2 XSS 攻击防护:过滤器拦截 + 全局处理
XSS 攻击的核心是前端没有做好输入校验,把用户提交的恶意脚本当作正常数据渲染到页面里。在这个系统里,主要风险点在客户姓名、备注、房源描述这些文本字段。比如一个客户备注里写了<script>alert('xss')</script>,如果前端直接渲染,就可能执行恶意脚本。
我的处理方案是加一个全局过滤器,对所有请求体的内容做转义处理,把特殊字符替换为安全实体:
public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; chain.doFilter(new XssWrapper(req), response); } } // XssWrapper 覆盖 getParameter 和 getInputStream 方法, // 把 <script>、<iframe>、javascript: 等内容替换为空或转义实体注意,这个过滤器里可以自定义白名单,比如富文本编辑器是允许<p>、<strong>这些标签存在的,需要在转义时做放行处理。过滤器能做大部分防护,但数据库层面的存储过程和安全参数化查询不能省,两条防线都要有。
4.3 文件上传的风险点:不只配置里改个大小就完事
系统里有房源照片上传、合同PDF上传、客户身份证照片上传,文件上传的安全隐患最容易在细节里翻车。
我在项目里做了这几个防护:
- 文件类型校验两处做:前端校验扩展名是基础,后端必须校验文件流的内容类型(MIME)和魔数,不能只信扩展名。一个
.jpg文件改成.html就能绕过前端校验的情况太常见了。 - 重命名存储:上传的文件直接存原文件名是非常危险的做法。我把文件名改成
UUID + 正确扩展名,避免路径穿越和文件名注入。 - 存储路径隔离:上传文件存放目录设置为静态资源隔离区域,不放在项目根目录(有执行权限的目录),避免上传恶意脚本后直接被服务器解析执行。
- 明确大小限制:
spring.servlet.multipart.max-file-size=10MB,防止大文件拖垮服务。
4.4 参数校验不能手写 if-else 堆起来
参数校验是最容易流于形式的部分。刚开始我习惯在 Controller 手写一堆if (param == null) {},写了几个接口就觉得不对劲——代码大量重复,而且容易漏。
后来我统一用了@Validated+javax.validation注解方案,在 DTO 上定义校验规则:
public class CustomerDTO { @NotBlank(message = "客户姓名不能为空") private String customerName; @NotBlank(message = "手机号不能为空") @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") private String phone; @NotNull(message = "意向等级不能为空") @Min(value = 1, message = "意向等级范围错误") @Max(value = 3, message = "意向等级范围错误") private Integer intentLevel; }然后通过@RestControllerAdvice统一处理MethodArgumentNotValidException,把具体的校验错误返回给前端。这样每个接口的参数规则都写得很直观,维护起来也比成堆的 if-else 舒服得多。
5. 实际开发中踩过的坑——这些问题课本上不一定写
5.1 MyBatis-Plus 字段映射的坑:驼峰和下划线
MyBatis-Plus 默认开启驼峰映射,create_time可以自动映射到实体类的createTime。但有一个情况会翻车:如果你手写了 XML 里的复杂查询,resultType 用的是 Map,那查出来的字段名就全是下划线风格(create_time),前端拿到的 JSON 字段名不一致,然后前端取不到值。
解决方案是统一约定:简单查询全部用 LambdaWrapper 和实体映射,复杂查询专门写 VO 类做映射,避免用 Map 接收结果。不要一会实体一会 Map,风格混着来会把自己绕晕。
5.2 分页查询里的 count 语句慢到离谱
MyBatis-Plus 分页插件会自动生成 count 语句,但房源列表带了很多关联条件的情况下,自动生成的 count 会把所有 join 表都统计一遍,数据量上来了就很拖速度。
我的处理办法:在复杂查询场景里,手工写一条轻量级的 count SQL,只统计主表的行数,配合分页查询使用:
<select id="countPage" resultType="long"> SELECT COUNT(*) FROM house_info h <where> <if test="status != null"> AND h.status = #{status}</if> <if test="keyword != null"> AND (h.building_name LIKE CONCAT('%', #{keyword}, '%') OR h.house_code LIKE CONCAT('%', #{keyword}, '%') )</if> </where> </select>5.3 日期时间处理上的"时区黑魔法"
这个坑我印象很深。我第一次部署完系统,测试的时候发现新增的房源创建时间比本地时间快了8个小时,数据库里存的时间完全不对。
原因很典型:MySQL 连接串没有配置serverTimezone,默认取的是服务器系统时区,而本地机器的时区是东八区,两边一错位就是8小时的偏差。
更隐蔽的是前端处理时间的方式。后端返回2024-01-15T10:30:00这种带T的 ISO 字符串,如果前端 JS 直接new Date(),展示的时候会发现时间又差了一截。我没有用复杂的全局序列化方案,而是统一在 VO 里用字符串输出格式化好的时间,后端承担时间格式化,前端拿来直接显示,反而少了很多问题:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;5.4 数据一致性不只靠代码,数据库的外键约束也不能省
有一个常见的观念是"外键会影响性能,所以不用"。在低并发管理系统的场景下,这个担心是多余的。我见过同事做的项目,房源、客户、合同三张表之间没有任何数据库约束,全靠 Java 代码自己保证关联关系。结果就是,合同表里能出现不存在的房源ID,客户删除的时候带看记录变成孤数据。
在这个项目里我在关键业务表之间建立了外键约束(合同表 → 房源表/客户表、带看记录表 → 客户表),并把删除策略设为RESTRICT——有关联数据时不允许直接删除。这个决定让数据完整性在数据库层面就托了底,代码里偶尔漏校验也不会出大事。
5.5 项目打包部署的几个注意点
打包部署是整个流程的最后一步,但很多人在这一步才暴露出问题。我用的是 Maven 打包成可执行 jar,部署前确认这几个点:
application.yml里的数据库地址、Redis 地址改成生产环境地址,连接密码不要用弱口令- 打包前先跑一遍测试,避免把测试环境的配置打进包里
- jar 包的启动方式建议写成脚本,
nohup java -jar estate.jar --spring.profiles.active=prod &,方便后台运行 - 服务器磁盘上上传目录要单独设置,不要放在
/tmp或者项目解压目录下,防止清理临时文件时误删
另外我还有个习惯:项目里放一个docs文件夹,把数据库初始化 SQL(建表语句+初始数据)和接口文档说明放在一起。打包或者交接的时候,别人照着文档能快速把环境跑起来,这个习惯在团队协作和毕设答辩时都很加分。
我个人对这个项目最深的体会是:管理系统这类项目的难点永远不在某一个技术点有多难,而在业务逻辑的完整性和严谨性。房源状态怎么流转、客户归属怎么隔离、合同金额怎么校验、并发操作怎么防重——这些才是真正花时间的地方。把业务链摸透了,Spring Boot 充其量只是把已经想清楚的设计翻译成代码而已。如果你正在做类似的系统,建议先花时间把需求场景、表结构和状态流转图画清楚,代码只是顺水推舟的事。