基于SSM的商铺租赁管理系统:技术栈、背景意义、核心代码与系统测试

1. 项目背景与意义

随着城市化进程的加快和商业地产的蓬勃发展,商铺租赁市场日益活跃,管理复杂度也随之提升。传统的人工记录、Excel表格或简单的单机软件管理方式,已难以满足现代商业地产公司、物业方及个体房东对租赁信息高效、精准、协同管理的需求。基于SSM(Spring + Spring MVC + MyBatis)框架开发的商铺租赁管理系统,旨在通过信息化手段,实现租赁流程的标准化、数据的集中化与业务的在线化,具有重要的现实意义:

  • 提升管理效率:自动化处理租约、账单、维修等流程,减少人工操作与错误。
  • 优化租户体验:为租户提供在线看房、合同查询、费用缴纳、报修申请等便捷服务。
  • 强化数据分析:集中管理租赁数据,为租金定价、空置率分析、收益预测提供数据支持。
  • 降低运营成本:通过流程优化减少人力与时间成本,规避因管理疏漏造成的财务损失。
  • 技术实践价值:作为经典的Java EE企业级应用,该项目融合了主流开源技术栈,是学习与掌握SSM框架、前后端分离、数据库设计及软件工程实践的优秀案例。

2. 技术栈详解

本系统采用典型的分层架构与主流Java技术生态,确保系统的可维护性、扩展性与性能。

2.1 后端技术栈

  • 核心框架:Spring 5.x + Spring MVC + MyBatis 3.x
  • 项目管理与依赖:Maven
  • Web容器:Apache Tomcat 9.x
  • 数据库:MySQL 8.0
  • 连接池:HikariCP 或 Druid
  • 事务管理:Spring 声明式事务(@Transactional)
  • JSON处理:Jackson
  • 日志框架:SLF4J + Logback
  • 单元测试:JUnit 5 + Mockito
  • API文档:Swagger2 / Knife4j(可选)

2.2 前端技术栈

  • 基础技术:HTML5, CSS3, JavaScript (ES6+)
  • UI框架:Bootstrap 5.x 或 Layui
  • 前端交互:jQuery 3.x + Ajax
  • 图表库:ECharts(用于数据可视化报表)
  • 模板引擎:Thymeleaf(服务端渲染)或纯前后端分离(Vue.js/React + RESTful API)

2.3 开发与部署工具

  • IDE:IntelliJ IDEA 或 Eclipse
  • 版本控制:Git
  • 数据库管理:Navicat 或 MySQL Workbench
  • 接口测试:Postman 或 Apifox

3. 系统核心模块与数据库设计

3.1 核心功能模块

  • 用户权限管理:系统管理员、物业经理、财务人员、租户等多角色登录与权限控制。
  • 商铺资源管理:商铺基本信息(编号、面积、位置、状态)、楼层平面图上传。
  • 租赁合同管理:合同创建、审批、续签、终止、到期提醒、电子归档。
  • 租金与费用管理:租金账单生成、水电物业费录入、在线支付记录、滞纳金计算。
  • 维修与投诉管理:租户报修/投诉提交、工单分配、处理进度跟踪、满意度评价。
  • 统计分析与报表:出租率统计、租金收入分析、费用明细报表、数据可视化图表。
  • 消息通知:合同到期、账单催缴、维修处理等系统内消息或邮件/SMS提醒。

3.2 核心数据库表(示例)

-- 用户表 CREATE TABLE `sys_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `role` varchar(20) NOT NULL COMMENT '角色(admin, manager, tenant, finance)', `status` tinyint(1) DEFAULT '1' COMMENT '状态(1正常 0禁用)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 商铺表 CREATE TABLE `shop` ( `shop_id` int(11) NOT NULL AUTO_INCREMENT, `shop_number` varchar(20) NOT NULL COMMENT '商铺编号', `floor` varchar(10) DEFAULT NULL COMMENT '所在楼层', `area` decimal(10,2) NOT NULL COMMENT '面积(㎡)', `monthly_rent` decimal(10,2) NOT NULL COMMENT '月租金', `status` varchar(20) DEFAULT 'vacant' COMMENT '状态(vacant, rented, under_repair)', `description` text COMMENT '描述', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`shop_id`), UNIQUE KEY `uk_shop_number` (`shop_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商铺信息表'; -- 租赁合同表 CREATE TABLE `lease_contract` ( `contract_id` int(11) NOT NULL AUTO_INCREMENT, `contract_no` varchar(50) NOT NULL COMMENT '合同编号', `shop_id` int(11) NOT NULL COMMENT '关联商铺ID', `tenant_id` int(11) NOT NULL COMMENT '租户用户ID', `start_date` date NOT NULL COMMENT '起租日', `end_date` date NOT NULL COMMENT '到期日', `total_amount` decimal(15,2) NOT NULL COMMENT '合同总金额', `payment_cycle` varchar(20) DEFAULT 'monthly' COMMENT '付款周期(monthly, quarterly, yearly)', `sign_date` date DEFAULT NULL COMMENT '签约日期', `status` varchar(20) DEFAULT 'active' COMMENT '状态(draft, active, expired, terminated)', `attachment_url` varchar(500) DEFAULT NULL COMMENT '合同附件路径', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`contract_id`), UNIQUE KEY `uk_contract_no` (`contract_no`), KEY `fk_contract_shop` (`shop_id`), KEY `fk_contract_tenant` (`tenant_id`), CONSTRAINT `fk_contract_shop` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`shop_id`), CONSTRAINT `fk_contract_tenant` FOREIGN KEY (`tenant_id`) REFERENCES `sys_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁合同表'; -- 租金账单表 CREATE TABLE `rent_bill` ( `bill_id` int(11) NOT NULL AUTO_INCREMENT, `contract_id` int(11) NOT NULL COMMENT '关联合同ID', `bill_no` varchar(50) NOT NULL COMMENT '账单编号', `bill_month` date NOT NULL COMMENT '账单所属月份', `rent_amount` decimal(10,2) NOT NULL COMMENT '租金金额', `utilities` decimal(10,2) DEFAULT '0.00' COMMENT '水电费', `management_fee` decimal(10,2) DEFAULT '0.00' COMMENT '管理费', `total_amount` decimal(10,2) NOT NULL COMMENT '账单总额', `due_date` date NOT NULL COMMENT '最晚付款日', `payment_status` varchar(20) DEFAULT 'unpaid' COMMENT '支付状态(unpaid, paid, overdue)', `payment_time` datetime DEFAULT NULL COMMENT '支付时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`bill_id`), UNIQUE KEY `uk_bill_no` (`bill_no`), KEY `fk_bill_contract` (`contract_id`), CONSTRAINT `fk_bill_contract` FOREIGN KEY (`contract_id`) REFERENCES `lease_contract` (`contract_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租金账单表';

4. 核心代码示例(Spring MVC + MyBatis)

4.1 实体类(Shop.java)

package com.example.leasing.entity; import java.math.BigDecimal; import java.util.Date; public class Shop { private Integer shopId; private String shopNumber; private String floor; private BigDecimal area; private BigDecimal monthlyRent; private String status; // vacant, rented, under_repair private String description; private Date createTime; // getters and setters 省略... }

4.2 Mapper接口与XML(ShopMapper.java & ShopMapper.xml)

package com.example.leasing.mapper; import com.example.leasing.entity.Shop; import org.apache.ibatis.annotations.Param; import java.util.List; public interface ShopMapper { int insert(Shop shop); int update(Shop shop); int deleteById(Integer shopId); Shop selectById(Integer shopId); List<Shop> selectAll(); List<Shop> selectByCondition(@Param("status") String status, @Param("floor") String floor); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.leasing.mapper.ShopMapper"> <resultMap id="BaseResultMap" type="com.example.leasing.entity.Shop"> <id column="shop_id" property="shopId"/> <result column="shop_number" property="shopNumber"/> <result column="floor" property="floor"/> <result column="area" property="area"/> <result column="monthly_rent" property="monthlyRent"/> <result column="status" property="status"/> <result column="description" property="description"/> <result column="create_time" property="createTime"/> </resultMap> <sql id="Base_Column_List"> shop_id, shop_number, floor, area, monthly_rent, status, description, create_time </sql> <insert id="insert" parameterType="Shop" useGeneratedKeys="true" keyProperty="shopId"> INSERT INTO shop (shop_number, floor, area, monthly_rent, status, description) VALUES (#{shopNumber}, #{floor}, #{area}, #{monthlyRent}, #{status}, #{description}) </insert> <select id="selectByCondition" resultMap="BaseResultMap"> SELECT <include refid="Base_Column_List"/> FROM shop WHERE 1=1 <if test="status != null and status != ''"> AND status = #{status} </if> <if test="floor != null and floor != ''"> AND floor = #{floor} </if> ORDER BY create_time DESC </select> </mapper>

4.3 Service层(ShopService.java)

package com.example.leasing.service; import com.example.leasing.entity.Shop; import java.util.List; public interface ShopService { boolean addShop(Shop shop); boolean updateShop(Shop shop); boolean deleteShop(Integer shopId); Shop getShopById(Integer shopId); List<Shop> getAllShops(); List<Shop> getShopsByCondition(String status, String floor); }
package com.example.leasing.service.impl; import com.example.leasing.entity.Shop; import com.example.leasing.mapper.ShopMapper; import com.example.leasing.service.ShopService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Transactional public class ShopServiceImpl implements ShopService { @Autowired private ShopMapper shopMapper; @Override public boolean addShop(Shop shop) { return shopMapper.insert(shop) > 0; } @Override public List<Shop> getShopsByCondition(String status, String floor) { return shopMapper.selectByCondition(status, floor); } // 其他方法实现类似,省略... }

4.4 Controller层(ShopController.java)

package com.example.leasing.controller; import com.example.leasing.entity.Shop; import com.example.leasing.service.ShopService; import com.example.leasing.vo.ResultVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/shop") public class ShopController { @Autowired private ShopService shopService; @PostMapping("/add") public ResultVO<Boolean> addShop(@RequestBody Shop shop) { boolean success = shopService.addShop(shop); return success ? ResultVO.success("添加成功") : ResultVO.error("添加失败"); } @GetMapping("/list") public ResultVO<List<Shop>> getShopList(@RequestParam(required = false) String status, @RequestParam(required = false) String floor) { List<Shop> list = shopService.getShopsByCondition(status, floor); return ResultVO.success("查询成功", list); } }

5. 系统测试方案

5.1 测试层次与策略

  • 单元测试(Unit Test):使用JUnit 5 + Mockito对Service层业务逻辑进行隔离测试,确保每个方法在各种输入下的正确性。
  • 集成测试(Integration Test):使用Spring TestContext框架,测试Controller层接口与数据库交互的整体流程,验证数据一致性。
  • 接口测试(API Test):使用Postman或Apifox编写测试集合,对RESTful API进行功能、参数边界、异常场景测试。
  • 系统测试(System Test):模拟真实用户操作,进行端到端(E2E)测试,覆盖核心业务流程(如:租户注册->查看商铺->签约->生成账单->支付)。
  • 性能测试(Performance Test):使用JMeter模拟高并发场景,测试关键接口(如账单生成、报表查询)的响应时间与吞吐量。
  • 安全测试(Security Test):检查SQL注入、XSS跨站脚本、越权访问等常见Web安全漏洞。

5.2 单元测试示例(ShopServiceTest.java)

package com.example.leasing.service.impl; import com.example.leasing.entity.Shop; import com.example.leasing.mapper.ShopMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ShopServiceImplTest { @Mock private ShopMapper shopMapper; @InjectMocks private ShopServiceImpl shopService; @Test void addShop_Success() { Shop shop = new Shop(); shop.setShopNumber("A-101"); shop.setArea(new BigDecimal("50.00")); shop.setMonthlyRent(new BigDecimal("5000.00")); shop.setStatus("vacant"); when(shopMapper.insert(any(Shop.class))).thenReturn(1); boolean result = shopService.addShop(shop); assertTrue(result); verify(shopMapper, times(1)).insert(shop); } @Test void getShopsByCondition_WithS