智慧社区项目开发(五)—— 小区管理模块前后端实现详解:从数据模型到业务逻辑
前端
一、功能概览:小区管理模块的核心能力
小区管理模块主要实现对小区信息的全生命周期管理,包含 5 个核心功能:
- 分页条件查询:支持按小区名称等条件查询,返回包含小区人数等扩展信息的列表
- 新增小区:提交小区基本信息(名称、经纬度等)
- 编辑小区:回显并修改已有小区信息
- 单条 / 批量删除:删除小区及关联数据(摄像头、居民等)
- 详情查询:通过 ID 获取小区详情用于编辑回显
二、前端实现:Vue 组件交互与数据流转
前端采用 Vue+Element UI 实现,核心涉及两个组件:Community.vue
(列表页)和add-or-update.vue
(新增 / 编辑弹窗)。我们重点解析这两个组件的交互逻辑与表单初始化细节。
1. 列表页与弹窗组件的 "显隐控制":不是路由跳转,而是组件切换
很多初学者会疑惑:点击 "添加" 或 "编辑" 按钮时,页面是如何从列表页切换到表单弹窗的?其实这里并没有使用路由跳转,而是通过组件显隐控制实现。
在Community.vue
(列表页)中,我们定义了addOrUpdateVisible
变量控制弹窗显示:
<!-- Community.vue 核心代码 -->
<template><div class="app-container"><!-- 列表与操作按钮 --><el-button @click="addOrUpdateHandle(1)">添加</el-button><el-button @click="addOrUpdateHandle()">编辑</el-button><!-- 新增/编辑弹窗组件:通过v-if控制显隐 --><add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="initData" /></div>
</template><script>
export default {data() {return {addOrUpdateVisible: false // 弹窗显示状态:false隐藏,true显示}},methods: {addOrUpdateHandle(flag) {this.addOrUpdateVisible = true; // 显示弹窗if (flag !== 1) {// 编辑场景:传递选中的小区ID给弹窗this.$nextTick(() => {this.$refs.addOrUpdate.init(this.ids[0].communityId);});} else {// 新增场景:不传递IDthis.$nextTick(() => {this.$refs.addOrUpdate.init();});}}}
}
</script>
核心逻辑:
- 点击 "添加" 或 "编辑" 按钮时,将
addOrUpdateVisible
设为true
,弹窗组件add-or-update
因v-if
条件满足而渲染 - 通过
this.$refs.addOrUpdate.init(...)
调用子组件的init
方法,传递小区 ID(编辑时)或空(新增时) - 弹窗关闭后,通过
@refreshDataList
事件通知父组件刷新列表数据
2. 表单初始化方法init(id)
:为什么先赋值 ID 再重置表单?
在add-or-update.vue
(弹窗组件)中,init(id)
方法是表单初始化的核心,先看代码:
<!-- add-or-update.vue 核心代码 -->
<script>
export default {data() {return {dataForm: {communityId: '',communityName: '',termCount: '',lng: '',lat: '',seq: '1'}}},methods: {init(id) {this.dataForm.communityId = id; // 先给communityId赋值this.visible = true; // 显示弹窗this.resetForm('dataForm'); // 重置表单this.dataForm.seq = 1; // 重置后单独设置seq默认值// 如果是编辑(有id),查询详情并回显if (this.dataForm.communityId) {getInfo(id).then(res => {if (res && res.code === 200) {this.dataForm.communityId = res.data.communityId;this.dataForm.communityName = res.data.communityName;// 其他字段赋值...}});}}}
}
</script>
疑问:先赋值this.dataForm.communityId = id
,再调用resetForm('dataForm')
,ID 不会被清空吗?
要理解这个问题,需明确两个关键点:
(1)resetForm
的作用:清除表单残留数据
resetForm
是 Element UI 提供的表单重置方法,作用是将表单字段恢复到初始状态(即data()
中定义的初始值)。在dataForm
中,communityId
的初始值是''
(空字符串),所以调用resetForm('dataForm')
后,communityId
会被重置为''
。
(2)为什么 ID 最终不会丢失?
这里的关键是条件判断与重新赋值:
- 新增场景:
id
为undefined
,执行this.dataForm.communityId = id
后值为undefined
,resetForm
后变为''
(不影响,新增无需 ID) - 编辑场景:
id
为具体数值(如 19),执行this.dataForm.communityId = id
后值为 19;resetForm
后变为''
,但随后进入if (this.dataForm.communityId)
判断 —— 这里的this.dataForm.communityId
看似是''
,但实际判断的是id
参数(因为resetForm
只修改了dataForm
,没修改id
变量)。
当getInfo(id)
接口返回数据后,会重新给this.dataForm.communityId
赋值(如res.data.communityId = 19
),因此最终 ID 会正确回显。
(3)为什么要先赋值再重置?
目的是避免表单残留旧数据。假设用户先编辑了 ID=19 的小区,关闭弹窗后又编辑 ID=20 的小区:如果不重置表单,表单中可能残留 ID=19 的名称、经纬度等数据,导致新数据被污染。resetForm
能确保每次编辑都是 "干净的开始",再通过接口获取最新数据回显,保证数据准确性。
后端
一、基础配置:分页插件与核心依赖
实现分页功能是列表查询的基础,MyBatis-Plus 提供了便捷的分页插件,需先完成配置。
1. 分页插件配置类
package com.qcby.community.configuration;import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration // 标识为配置类
public class PageConfiguration {@Bean // 将分页拦截器注入Spring容器public PaginationInterceptor paginationInterceptor(){return new PaginationInterceptor();}
}
作用说明:
PaginationInterceptor
是 MyBatis-Plus 的分页拦截器,会自动拦截带有Page
参数的查询方法- 配置后,无需手动编写
LIMIT
语句,框架会自动处理分页逻辑 - 支持 MySQL、Oracle 等多种数据库,自动适配不同数据库的分页语法
2. 核心依赖(Maven)
<!-- SpringBoot Web -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><!-- MyBatis-Plus -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3.4</version>
</dependency><!-- MySQL驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency><!-- Lombok(简化实体类) -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
二、数据模型设计:实体类与 VO
后端数据模型需区分数据库实体(Entity) 和视图对象(VO),前者对应数据库表结构,后者用于前端数据展示。
1. 数据库表结构(community 表)
CREATE TABLE `community` (`community_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '小区ID',`community_name` varchar(255) NOT NULL COMMENT '小区名称',`term_count` int(11) DEFAULT NULL COMMENT '楼栋数量',`seq` int(11) DEFAULT NULL COMMENT '排序',`creater` varchar(50) DEFAULT NULL COMMENT '创建人',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`lng` float DEFAULT NULL COMMENT '经度',`lat` float DEFAULT NULL COMMENT '纬度',PRIMARY KEY (`community_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小区表';
2. 实体类(Entity):Community
package com.qcby.community.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;@Data // Lombok注解:自动生成getter、setter、toString等方法
@TableName("community") // 指定对应数据库表名
public class Community {@TableId(type = IdType.AUTO) // 主键自增private Integer communityId;private String communityName;private Integer termCount;private Integer seq;private String creater;private Date createTime;private Float lng;private Float lat;
}
关键点:
@TableName
:当类名与表名不一致时,必须指定表名@TableId
:标记主键,type = IdType.AUTO
表示数据库自增- Lombok 的
@Data
注解大幅减少模板代码
3. 视图对象(VO):CommunityVO
由于前端列表需要personCnt
(小区人数)字段,而该字段不存在于community
表中,需定义 VO 用于数据返回:
package com.qcby.community.vo;import lombok.Data;
import java.util.Date;@Data
public class CommunityVO {private Integer communityId;private String communityName;private Integer termCount;private Integer seq;private String creater;private Date createTime;private Float lng;private Float lat;private Integer personCnt; // 小区人数(扩展字段)
}
VO 与 Entity 的区别:
- Entity 严格对应数据库表结构,用于持久化操作
- VO 根据前端需求定义,包含扩展字段,用于接口返回
- 通过
BeanUtils.copyProperties()
实现两者字段的快速复制
三、请求与响应封装
统一请求参数格式和响应格式,是后端接口规范的核心。
1. 请求参数封装:CommunityListForm
前端查询时传递的参数(页码、每页条数、查询条件)需封装为 Form 类:
package com.qcby.community.form;import lombok.Data;@Data
public class CommunityListForm {private Integer page; // 当前页码private Integer limit; // 每页条数private Integer communityId; // 小区ID(可选)private String communityName; // 小区名称(可选,用于模糊查询)
}
2. 分页结果封装:PageVO
分页查询的结果需包含总条数、总页数等信息,封装为PageVO
:
package com.qcby.community.vo;import lombok.Data;
import java.util.List;@Data
public class PageVO {private Long totalCount; // 总记录数private Long pageSize; // 每页条数private Long totalPage; // 总页数private Long currPage; // 当前页码private List list; // 分页数据列表
}
3. 统一响应格式:Result
所有接口返回统一格式的 JSON,便于前端处理:
package com.qcby.community.common;import lombok.Data;@Data
public class Result {private Integer code; // 状态码:200成功,其他失败private String msg; // 提示信息private Object data; // 响应数据(可选)// 成功响应(无数据)public static Result ok() {Result result = new Result();result.setCode(200);result.setMsg("操作成功");return result;}// 成功响应(带数据)public static Result ok(Object data) {Result result = new Result();result.setCode(200);result.setMsg("操作成功");result.setData(data);return result;}// 失败响应public static Result error(String msg) {Result result = new Result();result.setCode(500);result.setMsg(msg);return result;}// 链式调用:设置数据public Result put(String key, Object value) {// 实际实现可使用Map存储多个数据this.data = value;return this;}
}
四、持久层(Mapper):数据库操作
MyBatis-Plus 的BaseMapper
提供了基础 CRUD 方法,复杂查询需自定义 SQL。
1. CommunityMapper
package com.qcby.community.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qcby.community.entity.Community;// 继承BaseMapper<Community>,获得基础CRUD方法
public interface CommunityMapper extends BaseMapper<Community> {
}
BaseMapper 核心方法:
selectPage(Page<T> page, @Param("ew") Wrapper<T> queryWrapper)
:分页查询insert(T entity)
:新增updateById(T entity)
:根据 ID 更新deleteById(Serializable id)
:根据 ID 删除selectById(Serializable id)
:根据 ID 查询
2. PersonMapper(关联查询)
需查询小区对应的居民数量,定义PersonMapper
:
package com.qcby.community.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qcby.community.entity.Person;
import org.apache.ibatis.annotations.Select;public interface PersonMapper extends BaseMapper<Person> {// 自定义SQL:查询指定小区的居民数量@Select("select count(*) from person where community_id = #{communityId}")Integer getCountByCommunityId(Integer communityId);
}
自定义 SQL 说明:
- 使用
@Select
注解编写 SQL 语句,适用于简单查询 - 复杂查询建议使用 XML 映射文件(在 resources/mapper 目录下)
#{communityId}
是参数占位符,自动防止 SQL 注入
五、业务层(Service):核心逻辑处理
Service 层负责实现业务逻辑,协调多个 Mapper 完成复杂操作。
1. CommunityService 接口
package com.qcby.community.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.qcby.community.entity.Community;
import com.qcby.community.form.CommunityListForm;
import com.qcby.community.vo.PageVO;public interface CommunityService extends IService<Community> {// 分页条件查询小区列表PageVO communityList(CommunityListForm communityListForm);
}
说明:继承IService<Community>
,获得 MyBatis-Plus 提供的增强 CRUD 方法。
2. CommunityServiceImpl 实现类
package com.qcby.community.service.impl;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qcby.community.entity.Community;
import com.qcby.community.form.CommunityListForm;
import com.qcby.community.mapper.CommunityMapper;
import com.qcby.community.mapper.PersonMapper;
import com.qcby.community.service.CommunityService;
import com.qcby.community.vo.CommunityVO;
import com.qcby.community.vo.PageVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;@Service
public class CommunityServiceImpl extends ServiceImpl<CommunityMapper, Community> implements CommunityService {@Autowired // 注入PersonMapper,用于查询小区人数private PersonMapper personMapper;@Overridepublic PageVO communityList(CommunityListForm form) {// 1. 构建分页对象(页码从1开始)Page<Community> page = new Page<>(form.getPage(), form.getLimit());// 2. 构建查询条件QueryWrapper<Community> queryWrapper = new QueryWrapper<>();// 模糊查询:如果小区名称不为空,则添加条件queryWrapper.like(StringUtils.isNotBlank(form.getCommunityName()), "community_name", // 数据库字段名form.getCommunityName()); // 查询值// 3. 执行分页查询(MyBatis-Plus自动处理分页)IPage<Community> resultPage = this.baseMapper.selectPage(page, queryWrapper);// 4. 转换为VO列表(补充personCnt字段)List<CommunityVO> voList = new ArrayList<>();for (Community community : resultPage.getRecords()) {CommunityVO vo = new CommunityVO();// 复制基础字段(community到vo)BeanUtils.copyProperties(community, vo);// 查询小区人数并设置到VOvo.setPersonCnt(personMapper.getCountByCommunityId(community.getCommunityId()));voList.add(vo);}// 5. 封装分页结果PageVO pageVO = new PageVO();pageVO.setList(voList);pageVO.setTotalCount(resultPage.getTotal()); // 总记录数pageVO.setPageSize(resultPage.getSize()); // 每页条数pageVO.setCurrPage(resultPage.getCurrent()); // 当前页码pageVO.setTotalPage(resultPage.getPages()); // 总页数return pageVO;}
}
核心逻辑拆解:
- 分页对象构建:
Page<Community>
指定页码和每页条数 - 查询条件组装:
QueryWrapper
用于动态拼接 SQL 条件(如模糊查询) - 分页查询执行:
selectPage
方法返回包含分页信息的IPage
对象 - 数据转换:将
Community
列表转换为CommunityVO
列表,补充扩展字段 - 结果封装:将分页信息(总条数、总页数等)封装到
PageVO
六、控制层(Controller):接口暴露
Controller 层负责接收前端请求,调用 Service 处理,并返回响应结果。
package com.qcby.community.controller;import com.qcby.community.form.CommunityListForm;
import com.qcby.community.common.Result;
import com.qcby.community.service.CommunityService;
import com.qcby.community.vo.PageVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController // 标识为控制器,且返回JSON
@RequestMapping("/community") // 基础路径
public class CommunityController {@Autowiredprivate CommunityService communityService;/*** 分页条件查询小区列表* GET请求:/community/list*/@GetMapping("/list")public Result getList(CommunityListForm form) {PageVO pageVO = communityService.communityList(form);return Result.ok().put("data", pageVO);}/*** 添加小区* POST请求:/community/add*/@PostMapping("/add")public Result add(@RequestBody Community community, HttpSession session) {// 从session获取当前登录用户(简化示例)User user = (User) session.getAttribute("user");community.setCreater(user.getUsername());community.setCreateTime(new Date()); // 设置创建时间boolean success = communityService.save(community);return success ? Result.ok() : Result.error("添加失败");}/*** 根据ID查询小区(编辑回显)* GET请求:/community/info/{id}*/@GetMapping("/info/{id}")public Result info(@PathVariable Integer id) {Community community = communityService.getById(id);return community != null ? Result.ok().put("data", community) : Result.error("小区不存在");}/*** 修改小区* PUT请求:/community/edit*/@PutMapping("/edit")public Result edit(@RequestBody Community community) {boolean success = communityService.updateById(community);return success ? Result.ok() : Result.error("修改失败");}/*** 批量删除小区* DELETE请求:/community/del*/@DeleteMapping("/del")@Transactional // 事务管理:确保关联表数据同时删除public Result del(@RequestBody Integer[] ids) {try {// 1. 删除关联表数据(摄像头、居民等)cameraService.remove(new QueryWrapper<Camera>().in("community_id", ids));personService.remove(new QueryWrapper<Person>().in("community_id", ids));// 2. 删除小区表数据communityService.removeByIds(Arrays.asList(ids));return Result.ok();} catch (Exception e) {e.printStackTrace();return Result.error("删除失败");}}
}
Controller 关键注解:
@RestController
:组合@Controller
和@ResponseBody
,返回 JSON@RequestMapping
:指定基础请求路径@GetMapping
/@PostMapping
等:指定 HTTP 请求方法@PathVariable
:获取 URL 路径中的参数@RequestBody
:接收 JSON 格式的请求体@Transactional
:声明事务,确保操作的原子性