ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于 Spring Boot + Vue3 的商业店铺租赁招商平台设计与实现

基于 Spring Boot + Vue3 的商业店铺租赁招商平台设计与实现

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系名片!

一、项目背景与意义

随着城市商业地产的快速发展,商铺租赁与招商业务的需求日益增长。传统的招商方式依赖线下中介、电话沟通和纸质合同,存在信息不透明、房源更新滞后、招商周期长、租户与业主匹配效率低等问题。借助互联网平台整合商铺资源、在线展示、意向登记与合同管理,能够显著提升招商效率,降低沟通成本。

本项目基于 Spring Boot + Vue3 前后端分离架构,构建一个面向业主(招商方)、租户(承租方)和平台管理员三方的商业店铺租赁招商平台,实现商铺信息发布、检索筛选、在线预约看铺、租赁意向管理、合同与订单管理等核心业务闭环。

二、系统需求分析

2.1 角色划分

  • 平台管理员:负责用户管理、商铺审核、分类与区域维护、订单与合同监管、数据统计。
  • 业主(招商方):发布商铺信息、上传图片与视频、维护可租状态、处理租赁意向与预约看铺、签订合同。
  • 租户(承租方):浏览与筛选商铺、收藏对比、提交租赁意向、预约看铺、在线签约与支付。

2.2 核心功能需求

  1. 商铺管理:商铺的增删改查、多图上传、面积/租金/楼层/业态等属性维护、上下架状态控制。
  2. 检索与筛选:按区域、面积区间、租金区间、业态类型、关键词等多维度组合查询,支持分页与排序。
  3. 预约看铺:租户选择时间提交预约,业主确认或拒绝,形成预约记录与消息通知。
  4. 租赁意向与订单:租户提交租赁意向,业主审核通过后生成订单,记录租期、租金、押金等信息。
  5. 合同管理:基于订单生成电子合同,支持在线查看、签署状态跟踪与归档。
  6. 收藏与评价:商铺收藏、浏览记录、租后评价与评分。
  7. 消息通知:预约、意向、订单状态变更的站内消息提醒。
  8. 数据统计:商铺数量、成交订单、区域分布、租金趋势等可视化图表。

三、技术选型与整体架构

3.1 技术栈

层次技术选型说明
前端框架Vue3 + Vite组合式 API,构建速度快
UI 组件库Element Plus后台管理与表单组件
状态管理Pinia用户信息、购物车式意向管理
路由Vue Router 4动态路由与权限控制
HTTP 请求Axios统一拦截器与 Token 注入
后端框架Spring Boot 3RESTful 接口服务
持久层MyBatis-Plus简化 CRUD 与分页
数据库MySQL 8业务数据存储
缓存Redis登录态、热点商铺缓存
鉴权Spring Security + JWT无状态认证与角色权限
文件存储MinIO / 阿里云 OSS商铺图片与合同文件

3.2 系统架构

flowchart LR A[Vue3 前端] -->|Axios / JWT| B[Spring Boot 网关层] B --> C[用户与权限模块] B --> D[商铺与检索模块] B --> E[预约与意向模块] B --> F[订单与合同模块] B --> G[消息与统计模块] C --> H[(MySQL)] D --> H E --> H F --> H G --> H C --> I[(Redis)] D --> I D --> J[对象存储] F --> J

四、数据库设计

核心表围绕用户、商铺、预约、意向、订单与合同展开,主要字段设计如下。

-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role VARCHAR(20) NOT NULL COMMENT 'ADMIN/OWNER/TENANT', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 商铺表 CREATE TABLE shop ( id BIGINT PRIMARY KEY AUTO_INCREMENT, owner_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, region VARCHAR(50), address VARCHAR(200), business_type VARCHAR(50) COMMENT '餐饮/零售/办公等', area DECIMAL(10,2) COMMENT '面积(㎡)', floor VARCHAR(20), rent DECIMAL(10,2) COMMENT '月租金', deposit DECIMAL(10,2) COMMENT '押金', cover_img VARCHAR(255), description TEXT, status TINYINT DEFAULT 0 COMMENT '0待审核 1已上架 2已下架 3已租出', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 预约看铺表 CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, appoint_time DATETIME NOT NULL, remark VARCHAR(255), status TINYINT DEFAULT 0 COMMENT '0待确认 1已确认 2已拒绝 3已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 租赁订单表 CREATE TABLE lease_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, shop_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, owner_id BIGINT NOT NULL, start_date DATE, end_date DATE, rent DECIMAL(10,2), deposit DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已签约 3已完成 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

五、后端核心实现

5.1 项目结构

shop-lease-server ├── common 公共模块(统一响应、异常、工具) ├── security 认证鉴权(JWT、过滤器) ├── module-user 用户与角色 ├── module-shop 商铺与检索 ├── module-order 预约、意向、订单、合同 └── module-stat 统计与消息

5.2 统一响应与全局异常

@Data public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> fail(int code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } } @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result<Void> handleBusiness(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { return Result.fail(500, "系统繁忙,请稍后重试"); } }

5.3 商铺分页检索接口

@RestController @RequestMapping("/api/shop") @RequiredArgsConstructor public class ShopController { private final ShopService shopService; @GetMapping("/page") public Result<Page<ShopVO>> page(ShopQuery query) { return Result.ok(shopService.pageQuery(query)); } @PostMapping public Result<Long> create(@RequestBody @Valid ShopDTO dto) { return Result.ok(shopService.createShop(dto)); } } @Service @RequiredArgsConstructor public class ShopServiceImpl implements ShopService { private final ShopMapper shopMapper; @Override public Page<ShopVO> pageQuery(ShopQuery query) { LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>() .eq(Shop::getStatus, 1) .eq(StringUtils.hasText(query.getRegion()), Shop::getRegion, query.getRegion()) .eq(StringUtils.hasText(query.getBusinessType()), Shop::getBusinessType, query.getBusinessType()) .ge(query.getMinArea() != null, Shop::getArea, query.getMinArea()) .le(query.getMaxArea() != null, Shop::getArea, query.getMaxArea()) .ge(query.getMinRent() != null, Shop::getRent, query.getMinRent()) .le(query.getMaxRent() != null, Shop::getRent, query.getMaxRent()) .like(StringUtils.hasText(query.getKeyword()), Shop::getTitle, query.getKeyword()) .orderByDesc(Shop::getCreateTime); Page<Shop> page = shopMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), wrapper); return page.convert(ShopConverter::toVO); } }

5.4 JWT 认证过滤器

@Component @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); if (jwtUtil.validate(token)) { String username = jwtUtil.getUsername(token); UserDetails user = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }

六、前端核心实现

6.1 请求封装与拦截器

import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) request.interceptors.response.use( res => { const { code, message, data } = res.data if (code === 200) return data ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) }, err => { ElMessage.error(err.message || '网络异常') return Promise.reject(err) } ) export default request

6.2 商铺列表页(组合式 API)

<template> <div class="shop-list"> <el-form :inline="true" :model="query"> <el-form-item label="区域"> <el-select v-model="query.region" clearable> <el-option v-for="r in regions" :key="r" :label="r" :value="r" /> </el-select> </el-form-item> <el-form-item label="面积"> <el-input-number v-model="query.minArea" :min="0" /> <span> - </span> <el-input-number v-model="query.maxArea" :min="0" /> </el-form-item> <el-form-item> <el-button type="primary" @click="loadData">查询</el-button> </el-form-item> </el-form> <el-row :gutter="16"> <el-col v-for="item in list" :key="item.id" :span="6"> <el-card :body-style="{ padding: '0px' }" @click="goDetail(item.id)"> <img :src="item.coverImg" class="cover" /> <div class="info"> <h4>{{ item.title }}</h4> <p>{{ item.region }} · {{ item.area }}㎡ · {{ item.rent }}元/月</p> </div> </el-card> </el-col> </el-row> <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" layout="prev, pager, next, total" @current-change="loadData" /> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { useRouter } from 'vue-router' import { getShopPage } from '@/api/shop' const router = useRouter() const list = ref([]) const total = ref(0) const regions = ['朝阳区', '海淀区', '西城区', '东城区'] const query = reactive({ region: '', minArea: null, maxArea: null, pageNum: 1, pageSize: 12 }) const loadData = async () => { const data = await getShopPage(query) list.value = data.records total.value = data.total } const goDetail = id => router.push(`/shop/${id}`) onMounted(loadData) </script> <style scoped> .cover { width: 100%; height: 160px; object-fit: cover; } .info { padding: 12px; } </style>

6.3 路由与权限控制

const routes = [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/shop/:id', component: () => import('@/views/ShopDetail.vue') }, { path: '/owner', component: () => import('@/layout/OwnerLayout.vue'), meta: { roles: ['OWNER'] }, children: [ { path: 'shop', component: () => import('@/views/owner/ShopManage.vue') }, { path: 'appointment', component: () => import('@/views/owner/Appointment.vue') } ] } ] router.beforeEach((to, from, next) => { const userStore = useUserStore() const roles = to.meta.roles if (roles && !roles.includes(userStore.role)) { return next('/403') } next() })

七、关键业务流程

7.1 预约看铺流程

sequenceDiagram participant T as 租户 participant F as 前端 participant S as 后端 participant O as 业主 T->>F: 选择商铺与时间提交预约 F->>S: POST /api/appointment S->>S: 校验商铺状态与时间冲突 S-->>O: 推送预约消息 O->>S: 确认或拒绝 S-->>T: 返回处理结果通知

7.2 订单状态流转

  • 待支付 → 已支付:租户完成押金与首期租金支付。
  • 已支付 → 已签约:双方在线签署电子合同。
  • 已签约 → 已完成:租期结束或提前退租结算完成。
  • 任意状态 → 已取消:超时未支付或双方协商取消。

八、部署与运行

  1. 准备 MySQL 8 与 Redis,执行初始化 SQL 脚本。
  2. 修改后端application.yml中的数据源、Redis 与对象存储配置。
  3. 后端执行mvn clean package打包,运行java -jar shop-lease-server.jar。
  4. 前端执行npm install与npm run build,产物部署至 Nginx。
  5. Nginx 配置/api反向代理到后端服务,开启 gzip 与静态资源缓存。

九、总结与展望

本文围绕商业店铺租赁招商平台,完成了需求分析、技术选型、数据库设计以及前后端核心代码实现。系统采用 Spring Boot + Vue3 前后端分离架构,通过 JWT 实现无状态鉴权,借助 MyBatis-Plus 提升数据访问效率,并结合 Redis 与对象存储优化性能与资源管理。

后续可扩展的方向包括:引入 Elasticsearch 提升商铺检索体验、接入电子签章服务实现合规合同签署、增加地图找铺与 VR 看铺功能,以及基于用户行为数据构建智能推荐,进一步提升招商匹配效率。

返回列表