SpringBoot+Vue全栈开发:租房招聘平台实战

📅 发布时间:2026/8/21 8:27:57
SpringBoot+Vue全栈开发:租房招聘平台实战 1. 项目概述SpringBootVue全栈租房招聘平台这个基于SpringBootVue的全栈项目是一个整合了在线租房和招聘功能的综合管理平台。作为典型的Java全栈实战案例它采用了当前企业级开发中最主流的技术组合后端使用SpringBoot框架搭建RESTful API前端采用Vue.js实现响应式界面数据存储使用MySQL关系型数据库。整套系统从技术选型到架构设计都体现了现代Web开发的典型模式特别适合作为计算机相关专业的毕业设计或课程设计选题。我在实际开发这类平台时发现这类综合性管理系统最能锻炼全栈开发能力。它不仅要求开发者掌握前后端分离架构的实现还需要处理复杂的业务逻辑关联——比如租房模块的房源审核流程与招聘模块的职位发布机制虽然业务领域不同但在技术实现上共享着相同的权限控制和数据验证逻辑。这种多模块的整合正是企业级应用的典型特征。2. 技术架构解析2.1 后端技术栈设计SpringBoot作为后端核心框架其自动配置特性极大地简化了项目初始化工作。我通常会这样组织后端结构src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ ├── config/ # 配置类 │ │ ├── controller/ # 控制器层 │ │ ├── dto/ # 数据传输对象 │ │ ├── entity/ # 数据库实体 │ │ ├── repository/ # 数据访问层 │ │ ├── service/ # 业务逻辑层 │ │ └── Application.java # 启动类 │ └── resources/ │ ├── application.yml # 应用配置 │ ├── static/ # 静态资源 │ └── templates/ # 模板文件数据库设计方面MySQL的表结构需要同时支持租房和招聘两个业务模块。以租房模块为例核心表包括CREATE TABLE house ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 房源标题, price decimal(10,2) NOT NULL COMMENT 月租金, area int(11) NOT NULL COMMENT 面积(㎡), room_type varchar(20) NOT NULL COMMENT 户型, address varchar(200) NOT NULL COMMENT 详细地址, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态(0待审核1已上架2已下架), user_id bigint(20) NOT NULL COMMENT 发布人ID, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房源信息表;2.2 前端技术方案Vue前端项目通常采用如下目录结构src/ ├── api/ # 接口定义 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── utils/ # 工具函数 ├── views/ # 页面组件 │ ├── rental/ # 租房模块 │ └── job/ # 招聘模块 └── main.js # 应用入口一个典型的房源列表组件实现template div classhouse-list el-card v-foritem in list :keyitem.id classhouse-item div slotheader classclearfix span{{ item.title }}/span el-tag :typestatusMap[item.status].type stylefloat: right {{ statusMap[item.status].text }} /el-tag /div div classhouse-info div classinfo-item i classel-icon-location/i {{ item.address }} /div div classinfo-item i classel-icon-office-building/i {{ item.room_type }} | {{ item.area }}㎡ /div div classinfo-item price ¥{{ item.price }}/月 /div /div /el-card el-pagination current-changehandlePageChange :current-pagequery.page :page-sizequery.size layouttotal, prev, pager, next :totaltotal /el-pagination /div /template script import { getHouseList } from /api/rental export default { data() { return { list: [], total: 0, query: { page: 1, size: 10 }, statusMap: { 0: { text: 待审核, type: info }, 1: { text: 已上架, type: success }, 2: { text: 已下架, type: danger } } } }, created() { this.loadData() }, methods: { async loadData() { const res await getHouseList(this.query) this.list res.data.list this.total res.data.total }, handlePageChange(page) { this.query.page page this.loadData() } } } /script3. 核心功能实现细节3.1 租房模块关键技术点房源发布流程需要考虑以下几个技术要点富文本编辑与图片上传PostMapping(/upload) public Result uploadImages(RequestParam(files) MultipartFile[] files) { if (files null || files.length 0) { return Result.fail(请选择上传文件); } ListString urls new ArrayList(); for (MultipartFile file : files) { if (!file.isEmpty()) { try { String originalFilename file.getOriginalFilename(); String fileExt originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString() fileExt; // 实际项目中应使用云存储服务 Path path Paths.get(uploadPath, fileName); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); urls.add(/uploads/ fileName); } catch (IOException e) { log.error(文件上传失败, e); return Result.fail(文件上传失败); } } } return Result.success(urls); }地图选址集成 前端集成高德地图API实现地址选择initMap() { this.map new AMap.Map(map-container, { zoom: 13, center: [116.397428, 39.90923] }); this.marker new AMap.Marker({ position: this.map.getCenter(), draggable: true }); this.map.add(this.marker); // 拖动事件 this.marker.on(dragend, (e) { const lnglat e.lnglat; this.getAddress(lnglat.getLng(), lnglat.getLat()); }); // 点击事件 this.map.on(click, (e) { this.marker.setPosition(e.lnglat); this.getAddress(e.lnglat.getLng(), e.lnglat.getLat()); }); }3.2 招聘模块特殊处理职位发布与申请流程需要特别注意简历文件处理PostMapping(/apply) public Result applyJob(RequestParam Long jobId, RequestParam MultipartFile resume, RequestParam String coverLetter) { // 验证文件类型 String contentType resume.getContentType(); if (!application/pdf.equals(contentType) !application/msword.equals(contentType) !application/vnd.openxmlformats-officedocument.wordprocessingml.document.equals(contentType)) { return Result.fail(仅支持PDF或Word格式简历); } // 保存简历文件 String resumePath fileStorageService.store(resume); // 创建申请记录 JobApplication application new JobApplication(); application.setJobId(jobId); application.setUserId(SecurityUtil.getCurrentUserId()); application.setResumePath(resumePath); application.setCoverLetter(coverLetter); application.setApplyTime(new Date()); application.setStatus(0); // 待处理 jobApplicationRepository.save(application); return Result.success(申请已提交); }站内信通知系统public void sendNotification(Long userId, String title, String content) { Notification notification new Notification(); notification.setUserId(userId); notification.setTitle(title); notification.setContent(content); notification.setCreateTime(new Date()); notification.setRead(false); notificationRepository.save(notification); // WebSocket实时推送 messagingTemplate.convertAndSendToUser( userId.toString(), /queue/notifications, new NotificationDTO(notification) ); }4. 系统安全与性能优化4.1 安全防护措施接口权限控制 使用Spring Security实现基于角色的访问控制Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers(/api/auth/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/landlord/**).hasRole(LANDLORD) .antMatchers(/api/recruiter/**).hasRole(RECRUITER) .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }敏感数据加密public class PasswordUtil { private static final int SALT_LENGTH 16; private static final int ITERATIONS 10000; private static final int KEY_LENGTH 256; public static String encrypt(String rawPassword) { byte[] salt generateSalt(); byte[] hash pbkdf2(rawPassword.toCharArray(), salt); return Base64.getEncoder().encodeToString(salt) : Base64.getEncoder().encodeToString(hash); } public static boolean matches(String rawPassword, String encodedPassword) { String[] parts encodedPassword.split(:); byte[] salt Base64.getDecoder().decode(parts[0]); byte[] expectedHash Base64.getDecoder().decode(parts[1]); byte[] actualHash pbkdf2(rawPassword.toCharArray(), salt); return Arrays.equals(expectedHash, actualHash); } private static byte[] generateSalt() { SecureRandom random new SecureRandom(); byte[] salt new byte[SALT_LENGTH]; random.nextBytes(salt); return salt; } private static byte[] pbkdf2(char[] password, byte[] salt) { try { PBEKeySpec spec new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH); SecretKeyFactory skf SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); return skf.generateSecret(spec).getEncoded(); } catch (Exception e) { throw new RuntimeException(e); } } }4.2 性能优化实践缓存策略Service CacheConfig(cacheNames houseCache) public class HouseServiceImpl implements HouseService { Autowired private HouseRepository houseRepository; Override Cacheable(key #id) public House getById(Long id) { return houseRepository.findById(id).orElse(null); } Override CacheEvict(key #house.id) public void update(House house) { houseRepository.save(house); } Override Cacheable(key list: #query.page - #query.size) public PageHouse query(HouseQuery query) { SpecificationHouse spec (root, cq, cb) - { ListPredicate predicates new ArrayList(); if (StringUtils.isNotBlank(query.getKeyword())) { predicates.add(cb.like(root.get(title), % query.getKeyword() %)); } if (query.getMinPrice() ! null) { predicates.add(cb.ge(root.get(price), query.getMinPrice())); } if (query.getMaxPrice() ! null) { predicates.add(cb.le(root.get(price), query.getMaxPrice())); } return cb.and(predicates.toArray(new Predicate[0])); }; Pageable pageable PageRequest.of(query.getPage() - 1, query.getSize(), Sort.by(Sort.Direction.DESC, createTime)); return houseRepository.findAll(spec, pageable); } }SQL优化示例Repository public interface HouseRepository extends JpaRepositoryHouse, Long, JpaSpecificationExecutorHouse { Query(value SELECT h.* FROM house h LEFT JOIN favorite f ON h.id f.house_id AND f.user_id :userId WHERE h.status 1 ORDER BY CASE WHEN f.id IS NOT NULL THEN 0 ELSE 1 END, h.create_time DESC, nativeQuery true) PageHouse findWithFavoriteStatus(Param(userId) Long userId, Pageable pageable); Query(SELECT new com.example.dto.HouseStatDTO( COUNT(h), AVG(h.price), MAX(h.price), MIN(h.price)) FROM House h WHERE h.status 1) HouseStatDTO getStatistics(); }5. 项目部署与扩展建议5.1 多环境部署方案使用Profile区分环境 application-dev.yml:server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rental_job_dev?useSSLfalseserverTimezoneAsia/Shanghai username: devuser password: dev123 redis: host: localhost port: 6379 mail: host: smtp.dev.com username: noreplydev.com password: mail123application-prod.yml:server: port: 8080 compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 spring: datasource: url: jdbc:mysql://prod-db:3306/rental_job_prod?useSSLtrueserverTimezoneAsia/Shanghai username: ${DB_USER} password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 redis: host: redis-cluster port: 6379 password: ${REDIS_PASSWORD} mail: host: smtp.sendgrid.net username: apikey password: ${SENDGRID_API_KEY} cache: redis: time-to-live: 3600000 # 1小时5.2 扩展功能建议支付系统集成Service public class PaymentService { Autowired private OrderRepository orderRepository; public PaymentResponse createPayment(Long orderId, PaymentMethod method) { Order order orderRepository.findById(orderId) .orElseThrow(() - new BusinessException(订单不存在)); switch (method) { case ALIPAY: return createAlipayPayment(order); case WECHAT: return createWechatPayment(order); default: throw new BusinessException(不支持的支付方式); } } private PaymentResponse createAlipayPayment(Order order) { // 调用支付宝SDK创建支付 AlipayTradePagePayRequest request new AlipayTradePagePayRequest(); request.setReturnUrl(paymentConfig.getAlipayReturnUrl()); request.setNotifyUrl(paymentConfig.getAlipayNotifyUrl()); AlipayTradePagePayModel model new AlipayTradePagePayModel(); model.setOutTradeNo(order.getOrderNo()); model.setTotalAmount(order.getAmount().toString()); model.setSubject(订单支付- order.getOrderNo()); model.setProductCode(FAST_INSTANT_TRADE_PAY); request.setBizModel(model); try { String form alipayClient.pageExecute(request).getBody(); return new PaymentResponse(true, 创建成功, form); } catch (AlipayApiException e) { log.error(支付宝支付创建失败, e); return new PaymentResponse(false, 支付创建失败); } } Transactional public void handlePaymentNotify(PaymentNotifyDTO notifyDTO) { // 验证签名 if (!verifySignature(notifyDTO)) { throw new BusinessException(签名验证失败); } // 查询订单 Order order orderRepository.findByOrderNo(notifyDTO.getOutTradeNo()) .orElseThrow(() - new BusinessException(订单不存在)); // 检查金额 if (order.getAmount().compareTo(new BigDecimal(notifyDTO.getTotalAmount())) ! 0) { throw new BusinessException(金额不一致); } // 更新订单状态 order.setStatus(OrderStatus.PAID); order.setPaymentTime(new Date()); orderRepository.save(order); // 其他业务处理... } }即时通讯功能 使用WebSocket实现实时聊天Controller public class ChatController { Autowired private SimpMessagingTemplate messagingTemplate; MessageMapping(/chat/{roomId}) SendToUser(/queue/messages) public ChatMessage handleMessage(DestinationVariable String roomId, Payload ChatMessage message, Principal principal) { // 保存消息到数据库 message.setFromUser(principal.getName()); message.setTimestamp(new Date()); chatService.saveMessage(roomId, message); // 广播给房间内其他用户 messagingTemplate.convertAndSend(/topic/chat/ roomId, message); return message; } EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { StompHeaderAccessor headers StompHeaderAccessor.wrap(event.getMessage()); String sessionId headers.getSessionId(); String username headers.getUser().getName(); // 更新用户在线状态 userService.updateOnlineStatus(username, true); } EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headers StompHeaderAccessor.wrap(event.getMessage()); String username headers.getUser().getName(); // 更新用户离线状态 userService.updateOnlineStatus(username, false); } }6. 开发经验与避坑指南6.1 常见问题解决方案跨域问题处理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .exposedHeaders(Authorization) .allowCredentials(true) .maxAge(3600); } }日期时间处理Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { // 全局日期格式化 builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd))); builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(HH:mm:ss))); // 时区设置 builder.timeZone(TimeZone.getTimeZone(Asia/Shanghai)); // NULL值处理 builder.serializationInclusion(JsonInclude.Include.NON_NULL); }; } }6.2 开发调试技巧API文档生成 使用Swagger配置Configuration EnableSwagger2 public class SwaggerConfig { Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.example.controller)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()) .securitySchemes(Collections.singletonList(apiKey())) .securityContexts(Collections.singletonList(securityContext())); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(租房招聘平台API文档) .description(SpringBootVue全栈项目接口文档) .version(1.0) .build(); } private ApiKey apiKey() { return new ApiKey(Authorization, Authorization, header); } private SecurityContext securityContext() { return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.any()) .build(); } ListSecurityReference defaultAuth() { AuthorizationScope authorizationScope new AuthorizationScope(global, accessEverything); AuthorizationScope[] authorizationScopes new AuthorizationScope[1]; authorizationScopes[0] authorizationScope; return Collections.singletonList(new SecurityReference(Authorization, authorizationScopes)); } }前端调试技巧 在Vue项目中配置代理解决跨域// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }使用axios拦截器统一处理请求和响应// src/utils/request.js import axios from axios import { Message } from element-ui import store from /store import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( config { if (store.getters.token) { config.headers[Authorization] Bearer store.getters.token } return config }, error { console.log(error) return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message({ message: res.message || Error, type: error, duration: 5 * 1000 }) // 特殊状态码处理 if (res.code 401) { store.dispatch(user/logout).then(() { router.push(/login) }) } return Promise.reject(new Error(res.message || Error)) } else { return res } }, error { console.log(err error) Message({ message: error.message, type: error, duration: 5 * 1000 }) return Promise.reject(error) } ) export default service