1. 项目背景与核心价值
这个基于SpringBoot+Vue的在线租房和招聘平台管理系统,本质上是一个典型的"双业务线"企业级应用。我在2019年参与过一个类似的城市服务整合平台项目,当时最大的痛点就是如何在一个系统中优雅地处理两种完全不同业务逻辑的数据流。
现代城市服务类平台往往需要整合多种民生服务。租房和招聘看似两个独立场景,但都涉及用户身份验证、地理位置服务、支付结算等共性模块。采用SpringBoot+Vue的技术组合,可以实现前后端彻底解耦,后端用Java处理复杂业务逻辑,前端用Vue构建响应式界面,这种架构选择在2023年仍然是中大型企业级应用的主流方案。
2. 技术架构设计解析
2.1 整体架构分层
系统采用经典的三层架构,但针对双业务特点做了特殊设计:
code复制表示层(Vue) → 业务逻辑层(SpringBoot) → 数据访问层(MyBatis)
↑ ↑
API网关 消息队列
我在实际部署时发现,租房业务的峰值访问往往集中在早晚通勤时段,而招聘业务在工作日白天更活跃。因此架构中特别加入了基于Redis的流量调度模块,可以根据时段自动调整两个业务模块的资源分配。
2.2 数据库设计要点
MySQL表设计采用了"基础表+业务扩展表"的模式:
sql复制-- 用户基础表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`phone` varchar(20) NOT NULL,
`user_type` tinyint NOT NULL COMMENT '1房东 2租客 3招聘方 4求职者',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 租房业务扩展表
CREATE TABLE `rental_property` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`title` varchar(100) NOT NULL,
`geo_hash` varchar(12) NOT NULL COMMENT '地理位置编码',
`price` decimal(10,2) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_geo_hash` (`geo_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 招聘业务扩展表
CREATE TABLE `job_position` (
`id` bigint NOT NULL AUTO_INCREMENT,
`company_id` bigint NOT NULL,
`title` varchar(100) NOT NULL,
`salary_range` varchar(50) NOT NULL,
`job_type` tinyint NOT NULL COMMENT '1全职 2兼职 3实习',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这种设计既保证了核心数据的统一管理,又能灵活适应不同业务的特殊字段需求。特别要注意的是user_type字段使用了位运算存储多角色标识,一个用户可能同时是房东和求职者。
3. 核心功能实现细节
3.1 基于JWT的混合身份认证
系统需要同时处理普通用户和企业用户的认证,我采用了增强型的JWT方案:
java复制// JWT令牌增强处理
public class EnhancedJwtTokenUtil {
// 添加多角色支持
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
if(userDetails instanceof CustomUserDetails) {
CustomUserDetails customUser = (CustomUserDetails)userDetails;
claims.put("roles", customUser.getRoleFlags()); // 位掩码存储多角色
claims.put("bizTypes", customUser.getBusinessTypes()); // 可访问的业务线
}
return doGenerateToken(claims, userDetails.getUsername());
}
// 业务线访问控制
public boolean validateBusinessAccess(String token, BusinessType type) {
Claims claims = getAllClaimsFromToken(token);
List<String> bizTypes = claims.get("bizTypes", List.class);
return bizTypes.contains(type.name());
}
}
重要提示:JWT的签名密钥必须定期轮换,我们在生产环境使用AWS KMS进行密钥管理,避免硬编码在代码中。
3.2 地理位置服务集成
租房模块的核心竞争力在于位置服务,我们采用GeoHash算法实现附近房源搜索:
java复制public class GeoService {
private static final int PRECISION = 6; // 约1km精度
public List<Property> findNearbyProperties(double lat, double lng, int radius) {
String centerGeoHash = GeoHash.geoHashStringWithCharacterPrecision(lat, lng, PRECISION);
// 获取周围8个GeoHash区块(九宫格)
Set<String> nearbyHashes = GeoHash.neighbors(centerGeoHash);
nearbyHashes.add(centerGeoHash);
return propertyMapper.selectByGeoHashes(
new ArrayList<>(nearbyHashes),
lat, lng, radius);
}
}
实测中发现,单纯依赖GeoHash会导致边界附近的房源遗漏,因此我们在SQL中额外添加了Haversine公式计算精确距离:
sql复制SELECT *,
6371 * 2 * ASIN(SQRT(
POWER(SIN((? - latitude) * pi()/180 / 2), 2) +
COS(? * pi()/180) * COS(latitude * pi()/180) *
POWER(SIN((? - longitude) * pi()/180 / 2), 2)
)) AS distance
FROM rental_property
WHERE geo_hash IN (?)
HAVING distance < ?
ORDER BY distance
4. 前后端协作实践
4.1 API契约管理
我们使用Swagger + OpenAPI 3.0规范接口定义,但实际开发中发现文档与代码不同步的问题。最终采用以下解决方案:
- 在SpringBoot中使用
springdoc-openapi注解 - 构建时自动生成openapi.json
- Vue项目通过
openapi-typescript-codegen自动生成客户端代码
yaml复制# 接口版本控制示例
paths:
/api/v1/rental:
get:
tags: [Rental]
parameters:
- $ref: '#/components/parameters/geoHash'
/api/v1/job:
get:
tags: [Job]
parameters:
- $ref: '#/components/parameters/salaryRange'
4.2 文件上传优化
系统需要处理房源图片和简历文件上传,我们实现了以下优化措施:
- 前端采用分片上传,使用
vue-simple-uploader组件 - 后端用Spring的
@RequestPart接收分片 - 使用Redis记录分片上传状态
- 最后用Java NIO合并文件
javascript复制// Vue上传组件配置
export default {
methods: {
handleFileUpload(file) {
this.$uploader.addFile(file, {
chunkSize: 2 * 1024 * 1024, // 2MB分片
simultaneousUploads: 3,
testChunks: true
})
}
}
}
5. 性能优化实战
5.1 缓存策略设计
针对租房和招聘的不同特点,我们设计了差异化的缓存策略:
| 业务模块 | 缓存类型 | 过期时间 | 更新策略 |
|---|---|---|---|
| 房源列表 | Redis | 30分钟 | 手动清除 |
| 房源详情 | Caffeine | 1小时 | 写时更新 |
| 职位列表 | Redis | 2小时 | 定时刷新 |
| 公司信息 | Redis | 24小时 | 版本控制 |
特别对于房源数据,我们实现了二级缓存:
java复制public class PropertyCacheService {
@Cacheable(value = "property", key = "#id")
public Property getPropertyById(Long id) {
// 先查本地缓存
Property property = caffeineCache.get(id);
if(property == null) {
// 查Redis
property = redisTemplate.opsForValue().get("property:"+id);
if(property == null) {
// 查数据库
property = propertyMapper.selectById(id);
redisTemplate.opsForValue().set(
"property:"+id,
property,
30, TimeUnit.MINUTES);
}
caffeineCache.put(id, property);
}
return property;
}
}
5.2 MySQL查询优化
通过EXPLAIN分析发现房源列表查询的瓶颈在于联合查询,我们进行了以下优化:
-
添加复合索引:
sql复制ALTER TABLE rental_property ADD INDEX idx_search (geo_hash, price, create_time); -
使用覆盖索引优化:
sql复制SELECT id, title, price FROM rental_property WHERE geo_hash = ? AND price BETWEEN ? AND ? ORDER BY create_time DESC -
对大文本字段(如房源描述)采用垂直分表
6. 安全防护体系
6.1 OWASP Top 10防护
我们在项目中实施了全面的安全措施:
- SQL注入:MyBatis全部使用#{}参数绑定
- XSS:前端使用vue-sanitize过滤,后端Jackson配置HTML转义
- CSRF:Spring Security默认启用CSRF保护
- 越权访问:方法级
@PreAuthorize注解 - 敏感数据:简历手机号加密存储
java复制// 数据脱敏示例
public class DataMaskingUtil {
public static String maskPhone(String phone) {
if(StringUtils.isEmpty(phone)) return "";
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
// 权限控制示例
@PreAuthorize("hasRole('LANDLORD') or hasRole('ADMIN')")
@PostMapping("/property")
public ResponseEntity createProperty(@RequestBody PropertyDTO dto) {
// ...
}
6.2 日志审计方案
采用ELK栈实现全链路日志追踪:
- 使用Logback的MDC记录请求ID
- 敏感操作记录详细审计日志
- 前端错误日志通过API上报
- 日志格式统一为JSON便于分析
xml复制<!-- Logback配置示例 -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"rental-job-system","env":"${spring.profiles.active}"}</customFields>
</encoder>
</appender>
7. 部署与监控
7.1 容器化部署
使用Docker Compose编排服务:
yaml复制version: '3'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
frontend:
build: ./frontend
ports:
- "80:80"
mysql:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=property_job
volumes:
mysql_data:
7.2 监控指标
我们收集了以下关键指标:
- 应用性能:通过Micrometer暴露Prometheus指标
- 业务指标:日活用户、房源发布量、职位申请量
- 异常监控:Sentry收集前端错误,ELK收集后端异常
java复制// 自定义业务指标
@RestController
public class MetricsController {
private final Counter propertyCounter;
public MetricsController(MeterRegistry registry) {
this.propertyCounter = registry.counter("property.create.count");
}
@PostMapping("/property")
public void createProperty() {
propertyCounter.increment();
// ...
}
}
8. 典型问题解决方案
8.1 跨业务线事务处理
当用户同时支付租房押金和购买招聘增值服务时,需要跨业务线事务:
java复制@Transactional
public void crossBusinessPayment(PaymentDTO dto) {
// 扣减账户余额
accountService.debit(dto.getUserId(), dto.getAmount());
try {
if(dto.getRentalOrderId() != null) {
rentalService.processPayment(dto.getRentalOrderId());
}
if(dto.getJobServiceId() != null) {
jobService.activatePremiumService(dto.getJobServiceId());
}
} catch (Exception e) {
// 补偿操作
accountService.credit(dto.getUserId(), dto.getAmount());
throw e;
}
}
8.2 大数据量导出
招聘企业常需要导出大量简历数据,我们采用分页流式导出:
java复制@GetMapping(value = "/resumes/export", produces = "application/octet-stream")
public void exportResumes(HttpServletResponse response) {
response.setHeader("Content-Disposition", "attachment; filename=resumes.csv");
try(OutputStream os = response.getOutputStream();
CSVPrinter printer = new CSVPrinter(
new OutputStreamWriter(os),
CSVFormat.DEFAULT)) {
int page = 0;
int size = 1000;
List<Resume> batch;
do {
batch = resumeMapper.selectByPage(page, size);
for(Resume resume : batch) {
printer.printRecord(
resume.getId(),
resume.getName(),
DataMaskingUtil.maskPhone(resume.getPhone()));
}
page++;
} while(!batch.isEmpty());
}
}
9. 项目演进建议
经过三个迭代周期的开发,我认为系统还可以在以下方面进行增强:
- 引入GraphQL替代部分REST API,解决移动端定制字段需求
- 使用Elasticsearch实现更强大的搜索功能
- 增加微信小程序端,使用Taro框架实现多端统一
- 对房源图片实现智能审核,使用阿里云内容安全API
- 简历解析功能,支持PDF/docx自动提取关键信息
在数据库层面,当单表数据超过500万时,建议考虑分库分表。我们做过测试,按城市ID分片可以将查询性能提升3-5倍。
