1. 项目背景与核心需求
在房产交易市场日益活跃的今天,传统线下中介模式面临着效率低下、信息不透明等痛点。我去年为本地一家中型房产中介公司开发管理系统时,发现他们还在使用Excel表格管理房源,业务员之间经常发生客户抢单纠纷。这正是我们需要网上房屋中介管理系统的现实场景。
这个基于SSM+Vue的系统主要解决三个核心问题:
- 房源信息数字化管理(解决纸质档案易丢失问题)
- 客户跟进流程标准化(避免业务员抢单)
- 交易数据可视化分析(帮助管理者掌握业务动态)
技术选型上,后端采用SSM框架组合(Spring+SpringMVC+MyBatis),这是经过多个项目验证的JavaEE黄金搭档。Spring的IoC容器管理着整个系统的Bean生命周期,我们通过@Transactional注解优雅地处理了房源状态更新的并发问题。MyBatis的动态SQL特性则完美适配了房源多条件组合查询的业务场景。
前端选用Vue.js 2.x版本(考虑到团队技术储备和ElementUI的兼容性),其响应式数据绑定让房源状态实时更新变得异常简单。有次产品经理临时要求增加"房源热度排行榜",我们仅用v-for指令配合computed属性,两小时就实现了这个需求。
2. 系统架构设计详解
2.1 整体技术架构
系统采用经典的前后端分离架构,这是我经过三次架构迭代后的最终方案。初期尝试过JSP方案,但前后端耦合导致需求变更成本太高;第二次改用Thymeleaf,依然不够灵活。现在的架构清晰划分了职责边界:
code复制[前端层] Vue2 + ElementUI + Axios
↑↓ HTTP/JSON
[网关层] Nginx反向代理 + 负载均衡
↑↓
[应用层] SpringMVC + 自定义注解权限控制
↑↓
[持久层] MyBatis + PageHelper分页 + Redis缓存
↑↓
[数据层] MySQL主从复制 + ElasticSearch搜索
特别说明几个关键设计点:
- 使用Nginx做静态资源服务和API路由,配置了gzip压缩后,首页加载时间从3s降至800ms
- Redis不仅做缓存,还用其Geo模块实现了"附近房源"功能,经纬度查询性能比MySQL快20倍
- ElasticSearch处理全文搜索,配合IK分词器支持"朝阳公园 三居室"这类自然语言查询
2.2 数据库设计要点
房源表的设计经历了几次重大调整。最初采用单表结构,后来拆分为:
sql复制CREATE TABLE `house` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`title` VARCHAR(100) NOT NULL COMMENT '房源标题',
`geo_point` POINT NOT NULL SRID 4326 COMMENT '经纬度坐标',
SPATIAL INDEX(`geo_point`),
-- 其他字段...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `house_detail` (
`house_id` BIGINT PRIMARY KEY,
`description` TEXT COMMENT '详情描述',
`facilities` JSON COMMENT '配套设施',
FOREIGN KEY (`house_id`) REFERENCES `house`(`id`)
);
这个设计有几个精妙之处:
- 使用MySQL 8.0的SRID 4326标准存储GIS数据,配合ST_Distance_Sphere函数计算距离
- 将不常查询的大字段分离到detail表,提升主表查询效率
- JSON类型存储设施配置,前端可直接绑定到Vue组件
3. 核心功能模块实现
3.1 房源管理模块
房源CRUD看似简单,但实际开发中遇到不少坑。比如图片上传功能,我们最终采用的技术方案是:
java复制@PostMapping("/upload")
public Result upload(@RequestParam MultipartFile file) {
// 1. 校验文件类型
String contentType = file.getContentType();
if(!Arrays.asList("image/jpeg", "image/png").contains(contentType)){
return Result.error("仅支持JPEG/PNG格式");
}
// 2. 生成OSS路径:按日期分目录+UUID防重名
String path = "houses/" + LocalDate.now().toString()
+ "/" + UUID.randomUUID() + ".jpg";
// 3. 上传到阿里云OSS(实际项目应配置CDN加速)
ossClient.putObject(bucketName, path, file.getInputStream());
return Result.success(ossDomain + path);
}
前端配合的Vue组件关键代码:
vue复制<template>
<el-upload
:action="uploadUrl"
:before-upload="validateFile"
@success="handleSuccess">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">
只能上传jpg/png文件,且不超过2MB
</div>
</el-upload>
</template>
<script>
export default {
methods: {
validateFile(file) {
const isImage = ['image/jpeg', 'image/png'].includes(file.type);
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) this.$message.error('请上传图片文件!');
if (!isLt2M) this.$message.error('图片大小不能超过2MB!');
return isImage && isLt2M;
}
}
}
</script>
3.2 客户跟进功能
这个功能我们迭代了三个版本才稳定。最终方案采用状态机模式设计跟进流程:
java复制public enum FollowStatus {
INIT(0, "待联系"),
CONTACTED(1, "已联系"),
VIEWED(2, "已带看"),
NEGOTIATION(3, "谈判中"),
DEAL(4, "已成交"),
LOST(5, "已流失");
// 状态转移规则
private static final Map<FollowStatus, Set<FollowStatus>> transitions = Map.of(
INIT, Set.of(CONTACTED, LOST),
CONTACTED, Set.of(VIEWED, LOST),
// 其他状态转移规则...
);
public static boolean canTransfer(FollowStatus from, FollowStatus to) {
return transitions.getOrDefault(from, Set.of()).contains(to);
}
}
前端使用Vue的transition组件实现状态流转的动画效果:
vue复制<transition name="fade" mode="out-in">
<el-select
v-model="currentStatus"
@change="handleStatusChange"
:disabled="!canEdit">
<el-option
v-for="status in availableStatus"
:key="status.value"
:label="status.label"
:value="status.value">
</el-option>
</el-select>
</transition>
4. 典型问题与解决方案
4.1 跨域问题处理
开发初期被跨域问题困扰许久。我们的终极解决方案是在SpringBoot中配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080") // Vue开发地址
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
同时Nginx生产环境配置需要添加:
nginx复制location /api/ {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
# 其他配置...
}
4.2 权限控制方案
经过多次安全审计后,我们最终采用RBAC模型+前端路由过滤的方案:
后端权限拦截器关键代码:
java复制public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 1. 获取请求路径和方法
String uri = request.getRequestURI();
String method = request.getMethod();
// 2. 从JWT中解析用户角色
String token = request.getHeader("Authorization");
Claims claims = JwtUtil.parseToken(token);
List<String> roles = claims.get("roles", List.class);
// 3. 查询数据库验证权限
return permissionService.checkPermission(roles, uri, method);
}
前端通过vue-router的全局守卫实现路由拦截:
javascript复制router.beforeEach((to, from, next) => {
const requiredRoles = to.meta.roles;
if (!requiredRoles) return next();
const userRoles = store.getters.roles;
if (hasPermission(requiredRoles, userRoles)) {
next();
} else {
next('/403');
}
});
5. 性能优化实践
5.1 前端性能提升
通过webpack-bundle-analyzer分析发现ElementUI占用过大,于是进行按需加载:
javascript复制// babel.config.js
module.exports = {
plugins: [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
还实现了以下优化:
- 路由懒加载:
component: () => import('./House.vue') - 图片懒加载:使用vue-lazyload插件
- 启用Gzip压缩:通过compression-webpack-plugin
5.2 后端缓存策略
采用多级缓存方案:
- 热点数据使用Redis缓存,设置合理的过期时间
java复制@Cacheable(value = "house", key = "#id", unless = "#result == null")
public House getById(Long id) {
return houseMapper.selectById(id);
}
- 使用Caffeine做JVM本地缓存,解决缓存穿透问题
java复制LoadingCache<Long, House> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(id -> houseMapper.selectById(id));
- MyBatis二级缓存针对性地开启,特别注意事务提交后才生效
6. 部署与监控方案
6.1 容器化部署
使用Docker Compose编排服务:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
redis:
image: redis:6
command: redis-server --appendonly yes
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
前端部署方案:
dockerfile复制FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
6.2 监控系统搭建
使用Prometheus+Grafana监控体系:
- SpringBoot应用暴露actuator端点
yaml复制management:
endpoints:
web:
exposure:
include: "*"
metrics:
tags:
application: ${spring.application.name}
- 配置Grafana看板监控:
- JVM内存/线程指标
- MySQL连接池状态
- 接口QPS/耗时百分位
这套系统上线后,我们及时发现并解决了数据库连接泄漏问题,平均响应时间从320ms降至180ms。
