1. 售楼管理系统的真实痛点与选型逻辑
先聊点实际的。市面上叫“售楼管理系统”的东西不少,但真正能在中小型房企、代理销售公司里落地、让置业顾问和案场经理都愿意用的,并不多。很多团队要么在用Excel表格手动登记客户,要么买了套笨重的ERP却因为操作复杂、流程僵化被闲置。我见过不少案场,客户来访信息记在本子上,跟进记录散落在微信聊天记录里,成交数据月底靠人工汇总——这种状态下,所谓的“客户盘点”“渠道分析”基本靠拍脑袋。
这个项目标题里的关键词拆开看,其实就对应了几层核心需求:SpringBoot负责后端接口和业务逻辑,Vue负责前端交互界面,MySQL存数据,MyBatis管数据库操作。整套东西组合起来,要解决的是售楼业务里最典型的几个问题:客户怎么登记、跟进记录怎么留痕、成交数据怎么统计、房源销控怎么避免撞单。
选这套技术栈不是因为它流行,而是因为它是当前Java生态里最稳、最容易招到人维护、也最适合快速交付的组合。SpringBoot让配置和部署变得简单,内嵌Tomcat,打成jar包就能跑,不需要额外装容器;Vue对后端开发者来说上手门槛低,组件化开发适合做这种表单密集型的业务系统;MyBatis则把SQL控制权完全交还给你,做复杂的报表统计和条件查询时非常顺手——这一点在售楼系统里尤其重要,因为这个业务的查询条件组合非常多。
我见过不少项目用JPA,确实省事,但遇到需要手写多表关联、动态条件拼接的场景时,反而容易被框架限制。售楼系统的核心查询——比如“按区域、户型、预算区间、客户意向等级过滤可售房源”——用MyBatis的动态SQL实现,逻辑清清楚楚,性能也可控。
这套系统适合谁来参考?如果你是Java后端开发者想积累一个完整的业务项目,或者你所在的小团队正好接到类似的信息化需求,再或者你想把这个项目作为毕设/求职项目——那这篇文章里从表设计到权限控制、从房源销控到数据看板的拆解,都能给你提供一套可以直接落地的思路。下面我会结合我实际开发这类系统时的决策过程,讲清楚每一步“为什么这么设计”。
2. 项目整体设计与模块规划
2.1 业务模块划分
售楼管理系统的业务核心,本质上是围绕“房源”和“客户”两条主线展开的。房源是静态的资产数据,客户是动态的流动数据,两者通过“认购”“签约”“贷款”等业务动作关联起来。
我在设计这个项目时,把功能拆成了六个核心模块:
房源管理:负责楼栋、单元、房号的维护,以及房源状态的实时变更。状态一般分为:未售、认购中、已签约、已交房、保留(预留)。状态变更必须走审批流或者有权限控制,不能随便改——这一点后面细说。
客户管理:登记来访客户、公客/私客转换、跟进记录维护。这里面最关键的规则是“撞单保护”,比如某客户被置业顾问A登记后,B想抢,系统需要支持限定时间内(比如7天)只有A能跟进,超时未跟进自动转为公客。
交易管理:包含认购单、签约单、退房单、换房单等业务单据的处理。每笔交易都关联客户、房源、价格方案(如折扣、付款方式),并计算应收金额。
财务管理:处理定金、首付款、贷款回款、退款等各类款项记录,生成收款流水和应收应付报表。
统计分析:日报、周报、月报,重点看来访量、成交量、转化率、回款金额、渠道效果。这个模块做得好不好,直接决定了这套系统是“能用”还是“好用”。
系统管理:用户、角色、权限、操作日志。售楼系统涉及敏感数据,权限必须细粒度控制。
模块之间的数据流转是这样的:客户来访 → 登记为公客 → 置业顾问认领 → 跟进带看 → 选房认购 → 缴纳定金 → 签订合同 → 分期付款 → 交房。每一步都在系统里留痕,每一步的状态变更都会联动相关模块的数据。比如认购成功后,房源状态从“未售”变为“认购中”,同时生成一条待收款记录;签约完成后,状态再变为“已签约”。
2.2 为什么选择前后端分离而不是单体JSP
这个问题我单独拿出来说,是因为在售楼系统这个业务场景下,前后端分离不只是技术偏好,它有实实在在的业务价值。
第一,售楼案场的网络环境复杂。置业顾问可能在售楼处前台用电脑,也可能在样板间用平板,销售经理在外地出差用手机看数据。前后端分离之后,后端API天然支持多端复用,同一套接口既能被PC端Vue页面调用,也能被移动端H5、甚至未来的小程序调用。
第二,发布和迭代更灵活。售楼业务的促销活动、价格策略变动频繁,前端页面调整常有发生。前后端分离后,前端可以在不影响后端的情况下独立发布,后端接口的升级对用户无感。
第三,并行开发效率高。团队里后端和前端可以并行开发,只要提前约定好接口文档。我用Swagger自动生成API文档,联调效率提升明显。
单体的JSP方案虽然开发简单、部署方便,但长期维护下来,前后端代码耦合在一起,改一个按钮样式都可能影响后端逻辑,团队协作也容易互相阻塞。现在的硬件条件和网络带宽也足够支撑前后端分离的请求开销,所以这已经是当前JavaWeb项目的主流选择。
2.3 技术选型的几个关键考量
再细化一下技术选型的理由,方便你面试或答辩时能说清楚“为什么是它”。
SpringBoot 2.x:选2.x而不是3.x的核心原因,主要是生态兼容性更稳。很多第三方库、旧项目的代码片段、网上的资料,针对2.x的积累更多。实际做项目时不需要追求版本最新,稳定和被充分踩坑过最重要。
Vue 2或Vue 3:如果项目是近期新建的,优先Vue 3 + Element Plus;如果参考的模板是Vue 2 + Element UI,也不用着急升级,Vue 2的生态依然成熟稳定。关键在于你要会用Composition API(Vue 3)或Options API(Vue 2),理解组件通信方式。
MyBatis:前面说过了,动态SQL和多表联查是核心优势。配一个MyBatis Generator自动生成基础mapper,复杂查询手写SQL,开发效率高且可控。
MySQL 8.x:InnoDB引擎,支持事务和外键。注意字符集统一utf8mb4,因为要存客户姓名、地址等可能有生僻字或表情符号的数据,utf8mb4才能完整支持。
前端构建工具:Vue CLI(Webpack)或Vite。Vite在开发环境启动快、热更新流畅,但Vue CLI的稳定性更被广泛验证。我建议如果是团队协作的大项目用Vue CLI,个人快速原型用Vite都行。
这套组合串起来之后,整个项目的架构层次就是:前端Vue组件 → Axios发起HTTP请求 → SpringBoot Controller接收参数 → Service层处理业务逻辑 → Mapper(MyBatis)操作MySQL。请求响应全程JSON格式,清晰明了。
3. 核心功能实现与关键代码解析
3.1 数据库表设计:业务根基必须扎实
售楼系统的数据库表设计是整个项目成败的关键。我见过不少半途改表结构的项目,改到后面开发人员都崩溃,就是因为开始设计时没有考虑周全业务关系。这里给出我实际用过的核心表设计思路。
以MySQL为例,主要表有:
- building(楼栋表):id, name(如“一期1栋”), unit_count(单元数), floor_count(层数)
- house(房源表):id, building_id, house_no(房号), area(面积), unit_type(户型,如“三室两厅”), price(总价), status(0未售 1认购 2签约 3保留), floor, direction(朝向)
- customer(客户表):id, name, phone, id_card, requirement(需求描述), source(渠道来源), level(意向等级), status(公客/私客), owner_id(归属销售)
- follow_record(跟进记录表):id, customer_id, content(跟进内容), next_plan(下一步计划), create_by, create_time
- deal_order(认购/签约单表):id, order_no, customer_id, house_id, type(1认购 2签约), total_price, discount(折扣) , paid_amount(已付金额), status, create_time
- payment_record(回款记录表):id, order_id, amount, pay_type(定金/首付/按揭回款), pay_time, operator
这个设计的几个关键决策点:
房源表和楼栋表分离,是为了统计“整栋的去化率”方便。如果你把楼栋信息冗余到房源表里,统计时GROUP BY楼栋字段也能做,但归属关系维护起来很容易出错。
客户表里owner_id字段,标记客户当前归属哪个销售。这个字段是撞单保护的核心逻辑基础。公客时owner_id为NULL,任何人可以认领;被认领后,只有owner本人可以跟进。
交易单据的状态流转不要裸写状态更新,最好通过Service层方法统一处理,比如“认购”操作内部要同时更新:deal_order插入记录、house状态改为认购中、customer状态改为成交意向客户——这三个操作必须在一个事务里完成,防止数据不一致。
下面是交易模块创建认购单的核心Service代码,这个逻辑是交易功能的核心:
@Service @Transactional public class DealServiceImpl implements DealService { @Autowired private HouseMapper houseMapper; @Autowired private DealOrderMapper dealOrderMapper; @Autowired private CustomerMapper customerMapper; @Override public Result createSubscription(DealOrderDTO dto) { // 1. 验证房源状态 House house = houseMapper.selectById(dto.getHouseId()); if (house.getStatus() != 0) { return Result.error("该房源当前状态为:" + house.getStatusName() + ",不可认购"); } // 2. 验证客户状态 Customer customer = customerMapper.selectById(dto.getCustomerId()); if (customer.getStatus() != 0) { return Result.error("该客户当前不可认购"); } // 3. 创建认购单(订单编号规则:RG + yyyyMMdd + 随机序号) DealOrder order = new DealOrder(); order.setOrderNo(generateOrderNo("RG")); order.setCustomerId(dto.getCustomerId()); order.setHouseId(dto.getHouseId()); order.setType(1); // 计算实际成交价:总价 * 折扣 BigDecimal actualPrice = house.getPrice() .multiply(dto.getDiscountRate()) .setScale(2, RoundingMode.HALF_UP); order.setTotalPrice(actualPrice); order.setDiscount(dto.getDiscountRate()); order.setStatus(0); // 待付款 dealOrderMapper.insert(order); // 4. 更新房源状态为“认购中” house.setStatus(1); houseMapper.updateById(house); // 5. 更新客户状态为“已认购” customer.setStatus(1); customerMapper.updateById(customer); return Result.success(order); } }注意我标注了步骤(5):事务通过@Transactional管理。这里如果不用事务,一旦执行到第4步报错,第3步插入的订单就没有了“回滚”机制,会产生脏数据。这是这类系统的第一原则:多人同时操作同一房源时,事务和行级锁必须用上。
上面的第1步和第2步其实是并发检查。但即使加了状态校验,两个销售同时点击“认购”同一房源,也可能同时读到status=0然后都继续执行。所以更稳妥的方式是在更新房源时使用乐观锁:
UPDATE house SET status = 1 WHERE id = #{houseId} AND status = 0如果影响行数为0,说明有人先你一步操作了,直接提示失败即可。这是避免超卖和撞单的关键设计。
在实际开发中,我强烈建议你在house表上加一个version字段做乐观锁,或者在UPDATE语句的WHERE条件里带上当前状态,两种方式都可,后者的实现成本更低。两种方式我都用过,如果你的并发量不大(中小型案场),WHERE条件带上state足够;如果系统需要接入线上选房(并发会高一个量级),务必备上version字段乐观锁。
3.2 客户报备与防撞单逻辑
“撞单”是售楼案场最常见的纠纷来源。两个置业顾问同时声称“这个客户是我先接触的”。售楼系统的客户管理模块,核心功能之一就是做公客、私客的流转和保客规则。
我这样设计:
- 新登记的客户自动进入“公客池”,此时无归属。
- 置业顾问可以“认领”公客,认领后客户变为“私客”,归属该顾问。
- 私客在7天内没有跟进记录(follow_record没有新增),系统自动释放回公客池。
- 私客的跟进记录只有归属人可见,或者同一团队领导可见。
- 客户一旦成交,归属权锁定,不可再变更。
这几个规则中的“7天无跟进释放公客”是让系统“活”起来的关键。很多团队用系统不积极,就是因为客户被锁死在个人名下,其他人没法跟进,客户资源慢慢就僵尸化。自动释放机制能促进良性竞争,也倒逼置业顾问提高跟进频率。
实现上,可以把释放逻辑做成一个定时任务:
@Component public class CustomerReleaseTask { @Autowired private CustomerMapper customerMapper; // 每天凌晨2点执行一次 @Scheduled(cron = "0 0 2 * * ?") public void releaseExpiredCustomers() { // 查询超过7天无跟进记录的私客 List<Customer> expiredList = customerMapper.selectExpiredCustomers(7); for (Customer customer : expiredList) { customer.setOwnerId(null); customer.setStatus(0); // 转回公客 customerMapper.updateById(customer); } } }这里的selectExpiredCustomers(7)SQL怎么写是关键:查询条件是客户为私客、最后跟进时间早于7天前,或者压根没有跟进记录。用NOT EXISTS子查询处理这种情况比较合适,比LEFT JOIN再判断NULL更直观。
对于团队考核,跟进记录本身也是销售过程管理的重要依据。管理者可以查看某个销售的所有跟进记录,评估客户的意向度和销售的跟进质量。
3.3 房源状态变更的审批与控制
房源状态在售楼业务里非常敏感。未售房源如果被误操作改成签约,会直接影响后续所有销售动作;而保留房需要经理审批,不能由普通销售直接操作。
我设计了状态变更的几种角色权限:
| 操作 | 普通销售 | 销售经理 | 系统管理员 |
|---|---|---|---|
| 登记新客户 | ✅ | ✅ | ✅ |
| 认领公客 | ✅ | ✅ | ✅ |
| 房源状态修改(未售→保留) | ❌ | ✅ | ✅ |
| 创建认购单 | ✅ | ✅ | ✅ |
| 退款/废弃订单 | ❌ | ✅ | ✅ |
| 查看全案场数据报表 | ❌ | ✅ | ✅ |
权限控制按RBAC(基于角色的访问控制)模型实现,数据库里有user表、role表、permission表,用户与角色多对多,角色与权限多对多。前端通过路由守卫控制页面访问,后端在Controller层通过自定义注解+拦截器校验权限,两层都做,才能保证安全。
后端拦截器的权限校验核心代码:
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { return reject(response, "未登录或登录已过期"); } String realToken = token.substring(7); // 从Redis取出登录用户信息(登录时以token为key存储) Object userObj = redisTemplate.opsForValue().get("LOGIN_TOKEN:" + realToken); if (userObj == null) { return reject(response, "登录状态已失效"); } // 将用户信息放入Request上下文,供后续业务逻辑使用 LoginUser user = (LoginUser) userObj; request.setAttribute("loginUser", user); return true; } private boolean reject(HttpServletResponse response, String msg) throws IOException { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(Result.error(msg))); return false; } }登录态用Token存储到Redis,就是为了支持无状态的前后端分离。用户登录成功后生成一个UUID字符串作为token,存入Redis(有效期通常为2小时,可续期),前端后续请求在Header带上token,拦截器每次校验。不使用Session是因为前后端分离部署下,Session不好跨域同步。
3.4 数据看板与统计报表实现
报表是卖给管理层的东西,做得好不好,直接决定系统在项目里的地位。我做过一个统计口径被经理质疑“数据不对”,结果查下来是SQL里count去重没加DISTINCT——这种细节上的坑很踩人。
统计模块我重点做了四个看板:
今日案场数据:来访客户数、新增认购数、新增签约数、当日回款金额。主要给销售经理开晨会看。
渠道效果分析:不同客户来源(自然来访、老带新、中介、网络、电话)的来访量、转化率、成交占比。用于营销投放决策。
销售业绩排行:按置业顾问维度统计认购额、签约额、回款额。注意要区分认购签约的月份归属,比如客户在1月认购、2月签约,业绩应计在各自发生的月份。
库存去化分析:整体的去化率、各户型的去化率、各楼栋的去化率,辅助定价和推盘节奏。
写这类统计SQL时,注意在大数据量下的索引优化。比如统计按日分组的数据,如果表里有10万订单,没有索引的话一个GROUP BY就可能导致慢查询。给trade_time加上索引是最基本的优化手段,条件允许的话,可以用时间分区表。
这是渠道效果统计的Mapper接口SQL:
<select id="selectChannelStatistics" resultType="map"> SELECT c.source AS channel, COUNT(*) AS visitorCount, SUM(CASE WHEN d.id IS NOT NULL THEN 1 ELSE 0 END) AS dealCount, ROUND(SUM(CASE WHEN d.id IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS conversionRate FROM customer c LEFT JOIN deal_order d ON d.customer_id = c.id WHERE c.create_time BETWEEN #{startDate} AND #{endDate} GROUP BY c.source ORDER BY visitorCount DESC </select>这条SQL里的LEFT JOIN是关键——一个客户可能多次认购(换房、加购等),但我们统计成交客户时应该去重,用LEFT JOIN配合判断d.id非空即可按客户维度统计是否成交,而非订单条数。
4. 开发环境配置与项目部署
4.1 本地开发环境搭建
一个完整的售楼系统开发环境,我按下面这套配置,踩坑最少:
- JDK 1.8(或JDK 11)
- Maven 3.6+(配置阿里云镜像加速依赖下载)
- MySQL 8.0(字符集utf8mb4)
- Redis 6.x(用于token存储)
- Node.js 14+(跑前端)
- IDEA 或 Eclipse
- Navicat 或 命令行工具
说到IDEA,我非常推荐在你的开发机上配置好Lombok插件和MyBatisX插件(IDEA自带的MyBatis插件不如它好用,它能根据Mapper接口方法自动生成XML映射和SQL校验)。
Maven的pom.xml核心依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.18</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>SpringBoot的application.yml配置里,有个细节容易忽略:
spring: datasource: url: jdbc:mysql://localhost:3306/sale_mgr?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.salemgr.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这个配置很重要。数据库字段是下划线命名(比如house_no),Java实体是驼峰命名(houseNo),开了这个配置后MyBatis自动映射,不需要手写大量的resultMap。很多人忽略这个,结果写各种复杂的resultMap,代码量大且易错。
4.2 前端项目搭建与Vue核心组件
前端我推荐使用Vue CLI创建项目,安装Element UI组件库,通过axios封装请求管理器。前端目录组织上,按照“页面级组件在views下,业务通用组件在components下,API请求封装在api下”的原则拆开。
前端封装的axios请求管理器是核心基建,建议直接写成可复用模块:
// api/request.js import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动附带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器:统一处理异常 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { Message.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request开发环境中,axios的baseURL需要代理到后端地址,在vue.config.js中配置:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这样一个配置,前端开发时请求/api就会自动转发到后端服务,避免跨域问题。生产环境则可以通过Nginx把前后端部署在同域下,反向代理后端接口地址。
房源管理页面是典型的表格+表单+搜索组合,对Element UI的使用要求很高。一个攻坚点:房源的“销控表”视图。销控表是售楼行业的特有展示——把整栋楼的房源以类似楼层平面图的方式展示,不同颜色代表不同状态,销售一眼就能看出哪套还没卖。这个视图在技术上用CSS Grid或Flex布局,每个房源是一个卡片块,点击打开详情弹窗,并按房态控制按钮可用性(比如已认购的房源不可再认购)。这种视图带来的直观性远超传统表格,是在演示或面试时可以加分的亮点功能。
4.3 部署与上线:从开发机到服务器
本地开发完成后,部署上线有几个要点:
后端的部署比较简单,Maven直接打包成jar包:
mvn clean package -DskipTests然后放到服务器上运行:
nohup java -jar sale-mgr.jar --spring.profiles.active=prod > app.log 2>&1 &生产环境的配置要单独一份application-prod.yml,数据库、Redis都指向生产环境,密码不能写在代码里,使用环境变量注入。
前端的部署是构建dist目录:
npm run build生成的静态文件上传到Nginx的html目录下,配置Nginx把/api路径的请求代理到后端的8080端口:
server { listen 80; server_name your_domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意try_files $uri $uri/ /index.html这一行,是Vue Router使用history模式时必须写的。如果不加,刷新页面时路由找不到对应文件,会出现404。这是前端部署最常见的坑,我在这里踩过不止一次。
数据库初始化方面,用Navicat执行项目提供的SQL脚本即可,建议把脚本按版本管理(v1.0.sql、v1.1.sql),每次上线都有增量脚本,不要直接改线上表结构。开发阶段用show create table比对结构差异,上线后就要规范化迁移流程了。
5. 权限控制与安全管理细节
5.1 登录与Token管理
售楼系统的用户登录安全,需要三个层面的保护:
密码存储:密码必须使用BCrypt加密,不能是MD5。有人觉得MD5加盐就够了,但在算力面前,MD5的碰撞和彩虹表攻击风险太大。Spring Security的BCryptPasswordEncoder自带盐处理,直接用即可。
Token过期与续期:我用Redis存储token,有效期2小时。用户在活跃状态下,每次请求都刷新token的过期时间;如果超过2小时没有操作,token失效,需要重新登录。我采用的方式是继承了HandlerInterceptor里的afterCompletion,或者在后端增加一个过滤器来统一做续期逻辑。
多设备登录控制:售楼系统一般不需要多端同时登录。用“同一用户同一时间只保留最新一个token”的策略,当某用户在其他地方登录时,旧token直接从Redis删除,旧设备就无法再访问。这个策略的实现很简单:登录时先删除该用户ID的所有旧token,再创建新token存Redis。注意,这个策略需要约束,如果有需要(比如经理要同时看案场和后台),可以把限制改成只限制销售角色。
5.2 前端路由权限与菜单渲染
前端不能只靠隐藏按钮来控制权限,因为技术上是挡不住恶意用户直接调接口的。前端权限的意义在于“不给用户展示无权限的功能入口”,真正的防盗门在后端。
前端我用动态路由实现:用户登录后接口返回他的角色和权限列表,前端根据权限列表动态添加路由。菜单也根据权限动态渲染。比如销售经理登录后能看到“系统管理”菜单,普通销售看不到,菜单直接不渲染。
前端路由守卫代码:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') return } if (token && to.path === '/login') { next('/') return } next() })注意动态添加路由不能用router.beforeEach里的判断做持久化,应该登录时后端把用户角色返回,前端存到Vuex,然后在路由初始化时通过addRoute动态添加,否则刷新页面时权限路由会丢失。
5.3 操作日志与审计
售楼业务涉及金钱和敏感数据,操作日志是必须具备的能力。谁修改了房源价格、谁取消了订单、谁变更了客户归属——这些操作全部要记录。
我实现了一个简单的操作日志AOP切面:
@Aspect @Component public class OperatorLogAspect { @Autowired private OperatorLogMapper logMapper; @Around("@annotation(operatorLog)") public Object recordLog(ProceedingJoinPoint joinPoint, OperatorLog operatorLog) throws Throwable { // 记录开始时间 long start = System.currentTimeMillis(); // 执行原方法 Object result = joinPoint.proceed(); // 记录操作信息 OperatorLogEntity log = new OperatorLogEntity(); log.setMethod(joinPoint.getSignature().getName()); log.setParams(JSON.toJSONString(joinPoint.getArgs())); log.setStatus("SUCCESS"); log.setCostTime(System.currentTimeMillis() - start); log.setOperator((String) RequestHolder.get().getAttribute("username")); logMapper.insert(log); return result; } }操作日志同步写入MySQL一张独立的operator_log表,这样即使业务表被误操作修改,也可以通过日志追溯。我还会把日志表按月份分区或者定期清理,因为这类日志增长很快,但查询跨度一般不大。
6. 常见问题与排坑实录
6.1 数据一致性问题
问题现象:一个房源被两个客户同时认购成功,系统出现“一房两卖”的数据。
排查过程:最开始在Service里做了状态判断,但没加事务和并发控制。当时是测试环境两个浏览器同时提交,都读到了status=0,然后都往下执行了。
解决方案:在update语句的条件中加入status=0,影响行数为0则抛异常回滚。同时给Service方法加@Transactional,保证多表操作的原子性。
经验:这种并发问题在开发环境很难发现,因为单用户测试永远是顺序操作。一定要在代码审查阶段就关注“同一时间多个用户操作同一资源”的场景。售楼业务里,房源、客户、订单都是高并发争抢的目标。
6.2 跨域问题
问题现象:前端访问后端接口时,浏览器控制台报CORS错误。
排查过程:前后端分离部署在不同域下(前端在8080,后端在8081),浏览器默认禁止跨域请求。
解决方案:两种方案都行。开发环境用Vue的proxy代理转发解决(推荐,简单);生产环境统一通过Nginx反向代理,让前端和后端从浏览器视角看是同一域名,从根上避免跨域。除非特殊情况,不建议在后端全局开启CORS,因为会削弱安全性。
6.3 前端依赖安装缓慢或失败
问题现象:npm install长时间卡住或者直接失败。
解决方案:换npm镜像源。在项目根目录创建.npmrc文件,内容为registry=https://registry.npmmirror.com,然后重新安装。如果还有问题,删掉node_modules和package-lock.json后重装,这个操作解决过大量莫名其妙的依赖问题。
6.4 日期与时间字段的类型问题
问题现象:前端传日期字符串“2024-01-15”,后端接收时变成空值或者格式异常。
解决方案:统一约定日期时间格式为yyyy-MM-dd HH:mm:ss,JSON序列化时使用Jackson的配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8数据库里日期字段用datetime类型,实体类用java.util.Date或LocalDateTime。使用LocalDateTime需要注意前端传来的格式必须包含时分秒,或者反序列化器的不足会导致LocalDateTime报错。
6.5 Excel导出出现中文乱码
问题现象:导出客户列表Excel时,中文全部乱码。
解决方案:HttpServletResponse设置响应头时指定编码:
response.setCharacterEncoding("UTF-8"); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("客户列表.xlsx", "UTF-8"));6.6 问题排查速查表
| 现象 | 直接原因 | 排查方向 |
|---|---|---|
| 接口返回401 | token失效或缺失 | 检查请求头Authorization、Redis中token的状态 |
| 新增/修改无效果 | SQL没有接收参数 | 检查Mapper XML中#{}占位符与实体类字段名是否匹配 |
| 登录失败 | 密码不匹配 | 确认使用BCrypt加密,而非MD5 |
| 页面白屏 | 路由配置错误或构建产物缺失 | 检查dist目录是否存在,Nginx try_files配置 |
| 定时任务不执行 | 漏配@EnableScheduling | 在启动类上加上@EnableScheduling注解 |
| 数据库连接超时 | 连接池耗尽 | 检查连接数配置、慢SQL、数据库连接回收参数 |
7. 项目的扩展方向与我的实战心得
最近在做一个二次扩展时,我把销售数据通过ECharts做成可视化大屏,让销售经理在案场大屏幕上实时看到来访量、去化率、业绩排行,效果比传统报表好很多。这是这个项目很好的扩展方向之一,成本不高但业务价值感很强。
另一个值得扩展的是移动端。现在置业顾问在现场接待客户时,基本都带着手机或平板。做一个轻量级的移动端H5,覆盖客户登记、跟进记录、房源速览三个高频场景,完全够日常使用。技术方案也不复杂:Vue端写一套响应式页面,复用现有的后端API,或者直接用uni-app打包成小程序。只要后端接口设计时保证了字段的完整性,移动端的扩展会非常顺畅。
再一个更有业务深度的方向是客户画像和意向度预测。售楼系统里积累了大量客户数据:来源渠道、浏览过的房源、跟进记录、成交与否。如果把这些数据抽取出来,做一个简单的“成交意向评分模型”,辅助置业顾问判断优先跟进哪些客户,这会是从“记录工具”走向“决策工具”的关键一步。起步不需要机器学习,用打分规则就行——比如最近7天有跟进加10分、看过房源加20分、客户主动咨询加30分——规则简单,但已经能起到排序和提醒的作用。后续如果数据量够,再引入更复杂的模型。
我在实际开发这类系统中的体会是:业务的“潜规则”比技术本身更容易被低估。比如售楼的折扣审批流程、按揭回款的时间节点、客户归属的争议处理——这些规则如果理解不到位,系统做出来“技术上没毛病、业务上用不了”。所以每做一个功能,我都会先问:“这个动作在真实案场里是什么场景?谁来做?走什么流程?谁会卡住?”想清楚了这些问题,代码写起来就顺利得多。
最后再分享一个开发效率方面的小技巧:这类系统其实重复度很高,建一套自己的代码生成器非常值。用MyBatis Generator或者自己写个简单的模板工具,根据表结构自动生成实体类、Mapper接口、XML文件,能省掉一半体力活。我后来的项目都是先生成基础代码,再手工补充业务逻辑,开发周期至少缩短三分之一。如果你正打算动手做这个售楼系统,我建议你先设计好表结构——那可是最花心思、也最影响后期开发效率的一步。