2026/10/29 21:57:48

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

基于 Spring Boot + Vue3 的商业店铺租赁招商平台设计与实现 温馨提示本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系名片一、项目背景与意义随着城市商业地产的快速发展商铺租赁与招商业务的需求日益增长。传统的招商方式依赖线下中介、电话沟通和纸质合同存在信息不透明、房源更新滞后、招商周期长、租户与业主匹配效率低等问题。借助互联网平台整合商铺资源、在线展示、意向登记与合同管理能够显著提升招商效率降低沟通成本。本项目基于 Spring Boot Vue3 前后端分离架构构建一个面向业主招商方、租户承租方和平台管理员三方的商业店铺租赁招商平台实现商铺信息发布、检索筛选、在线预约看铺、租赁意向管理、合同与订单管理等核心业务闭环。二、系统需求分析2.1 角色划分平台管理员负责用户管理、商铺审核、分类与区域维护、订单与合同监管、数据统计。业主招商方发布商铺信息、上传图片与视频、维护可租状态、处理租赁意向与预约看铺、签订合同。租户承租方浏览与筛选商铺、收藏对比、提交租赁意向、预约看铺、在线签约与支付。2.2 核心功能需求商铺管理商铺的增删改查、多图上传、面积/租金/楼层/业态等属性维护、上下架状态控制。检索与筛选按区域、面积区间、租金区间、业态类型、关键词等多维度组合查询支持分页与排序。预约看铺租户选择时间提交预约业主确认或拒绝形成预约记录与消息通知。租赁意向与订单租户提交租赁意向业主审核通过后生成订单记录租期、租金、押金等信息。合同管理基于订单生成电子合同支持在线查看、签署状态跟踪与归档。收藏与评价商铺收藏、浏览记录、租后评价与评分。消息通知预约、意向、订单状态变更的站内消息提醒。数据统计商铺数量、成交订单、区域分布、租金趋势等可视化图表。三、技术选型与整体架构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 ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT fail(int code, String message) { ResultT r new Result(); r.setCode(code); r.setMessage(message); return r; } } RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusiness(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { return Result.fail(500, 系统繁忙请稍后重试); } }5.3 商铺分页检索接口RestController RequestMapping(/api/shop) RequiredArgsConstructor public class ShopController { private final ShopService shopService; GetMapping(/page) public ResultPageShopVO page(ShopQuery query) { return Result.ok(shopService.pageQuery(query)); } PostMapping public ResultLong create(RequestBody Valid ShopDTO dto) { return Result.ok(shopService.createShop(dto)); } } Service RequiredArgsConstructor public class ShopServiceImpl implements ShopService { private final ShopMapper shopMapper; Override public PageShopVO pageQuery(ShopQuery query) { LambdaQueryWrapperShop 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); PageShop 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 request6.2 商铺列表页组合式 APItemplate div classshop-list el-form :inlinetrue :modelquery el-form-item label区域 el-select v-modelquery.region clearable el-option v-forr in regions :keyr :labelr :valuer / /el-select /el-form-item el-form-item label面积 el-input-number v-modelquery.minArea :min0 / span - /span el-input-number v-modelquery.maxArea :min0 / /el-form-item el-form-item el-button typeprimary clickloadData查询/el-button /el-form-item /el-form el-row :gutter16 el-col v-foritem in list :keyitem.id :span6 el-card :body-style{ padding: 0px } clickgoDetail(item.id) img :srcitem.coverImg classcover / div classinfo h4{{ item.title }}/h4 p{{ item.region }} · {{ item.area }}㎡ · {{ item.rent }}元/月/p /div /el-card /el-col /el-row el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal layoutprev, pager, next, total current-changeloadData / /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; } /style6.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 订单状态流转待支付 → 已支付租户完成押金与首期租金支付。已支付 → 已签约双方在线签署电子合同。已签约 → 已完成租期结束或提前退租结算完成。任意状态 → 已取消超时未支付或双方协商取消。八、部署与运行准备 MySQL 8 与 Redis执行初始化 SQL 脚本。修改后端application.yml中的数据源、Redis 与对象存储配置。后端执行mvn clean package打包运行java -jar shop-lease-server.jar。前端执行npm install与npm run build产物部署至 Nginx。Nginx 配置/api反向代理到后端服务开启 gzip 与静态资源缓存。九、总结与展望本文围绕商业店铺租赁招商平台完成了需求分析、技术选型、数据库设计以及前后端核心代码实现。系统采用 Spring Boot Vue3 前后端分离架构通过 JWT 实现无状态鉴权借助 MyBatis-Plus 提升数据访问效率并结合 Redis 与对象存储优化性能与资源管理。后续可扩展的方向包括引入 Elasticsearch 提升商铺检索体验、接入电子签章服务实现合规合同签署、增加地图找铺与 VR 看铺功能以及基于用户行为数据构建智能推荐进一步提升招商匹配效率。