前言这段时间一直在做智慧社区项目之前完结的两篇已经把登录的整个流程和动态路由开发过程整理清楚有兴趣的小伙伴可以看小编本专栏的AI智慧社区开头的两篇文章。今天终于把首页的社区统计柱状图接口完整写完、测通了。从对着开发文档一点点理解需求到分层写代码、排查报错、再到前后端顺利联调整个过程踩了不少坑也实实在在学到了东西。我把完整的开发思路、每一步代码和自己踩过的易错点都整理出来既是给自己做一次复盘也希望能帮到和我一样正在学习 Spring Boot 接口开发的同学跟着流程就能从零写出一个能用、规范、不报错的图表接口。因为还有很多朋友是第一次看我的文章所以博主还是坚持老习惯上来先带大家捋顺对照接口开发的思路和本项目的技术栈老朋友可以直接看第二节开发步骤。一、接口开发核心思路1. 需求分析对照开发文档开发目标实现/sys/inOut/chart接口返回符合以下格式的 JSON 数据用于前端柱状图渲染{ msg: 操作成功, code: 200, data: { names: [社区1, 社区2, 社区3], // 社区名称数组 nums: [5, 3, 1] // 对应社区的统计数值 } }核心业务逻辑统计in_out_record表中各社区的出入记录数量关联community表获取社区名称。2.技术栈与核心术语解释技术 / 术语通俗解释作用Spring Boot简化 Spring 开发的框架快速搭建 Java 后端项目自动配置 Tomcat、MyBatis 等MyBatis持久层框架实现 Java 对象与数据库的映射简化 SQL 操作Controller控制器接收前端请求调用 Service 层返回响应数据Service/ServiceImpl服务层处理业务逻辑Controller 和 Mapper 的中间层Mapper/XML数据访问层定义数据库操作接口XML 编写具体 SQLResult 封装类统一响应格式规范接口返回值包含 code/msg/dataRESTful API接口设计风格通过 URLHTTP 方法定义接口本例用 GET 请求二、开发步骤一步步教你写1.创建 Controller 层接收请求核心作用作为前端与后端的入口接收 HTTP 请求调用 Service 层处理业务最终返回格式化响应。package com.qcby.smartcommunity.controller; import com.qcby.smartcommunity.service.InOutService; import com.qcby.smartcommunity.util.Result; // 统一响应封装类 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; import java.util.Map; /** * 出入记录图表数据接口控制器 * RestController组合注解 Controller ResponseBody返回JSON而非页面 * RequestMapping定义接口根路径 /sys/inOut */ RestController RequestMapping(/sys/inOut) public class InOutController { // 自动注入Service层对象依赖注入 Autowired private InOutService inOutService; /** * 柱状图数据接口 * GetMapping指定GET请求接口路径 /chart * return 统一格式的响应结果 */ GetMapping(/chart) public Result getChart() { // 调用Service层获取数据 MapString, Object data inOutService.getChartData(); // 封装成统一响应格式返回Result.ok()表示成功put添加数据 return Result.ok().put(data, data); } }2.创建 Service 接口定义业务方法核心作用定义业务逻辑的抽象方法解耦 Controller 与具体实现。package com.qcby.smartcommunity.service; import java.util.Map; /** * 出入记录服务接口 * 定义业务方法具体实现在ServiceImpl中 */ public interface InOutService { /** * 获取图表数据 * return 包含names和nums的Map */ MapString, Object getChartData(); }3.创建 Service 实现类处理业务逻辑核心作用实现 Service 接口调用 Mapper 层查询数据库处理数据格式转换。package com.qcby.smartcommunity.service.impl; import com.qcby.smartcommunity.mapper.InOutMapper; import com.qcby.smartcommunity.service.InOutService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Service接口的具体实现 * Service标记为Spring服务组件让Spring管理 */ Service public class InOutServiceImpl implements InOutService { // 注入Mapper层对象 Autowired private InOutMapper inOutMapper; Override public MapString, Object getChartData() { // 1. 调用Mapper查询数据库获取社区名称和对应数量 ListMapString, Object list inOutMapper.countByCommunity(); // 2. 空值保护避免null导致前端报错 if (list null) { list new ArrayList(); } // 3. 数据格式转换适配前端要求 ListString names new ArrayList(); // 社区名称数组 ListInteger nums new ArrayList(); // 数量数组 for (MapString, Object item : list) { // 从查询结果中获取社区名称和数量 names.add((String) item.get(name)); nums.add(((Number) item.get(count)).intValue()); } // 4. 封装返回数据 MapString, Object result new HashMap(); result.put(names, names); result.put(nums, nums); return result; } }4.创建 Mapper 接口定义数据库操作核心作用定义数据库查询方法MyBatis 通过动态代理生成实现类。package com.qcby.smartcommunity.mapper; import org.apache.ibatis.annotations.Mapper; import java.util.List; import java.util.Map; /** * 出入记录数据访问接口 * Mapper标记为MyBatis的Mapper接口让MyBatis扫描并生成代理类 */ Mapper public interface InOutMapper { /** * 按社区统计出入记录数量 * return 包含name(社区名)和count(数量)的Map列表 */ ListMapString, Object countByCommunity(); }5.编写 Mapper XML实现 SQL 查询核心作用编写具体的 SQL 语句实现数据库查询。?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd !-- namespace必须与Mapper接口全类名一致 -- mapper namespacecom.qcby.smartcommunity.mapper.InOutMapper !-- id与Mapper接口中的方法名一致 resultType返回类型map表示返回键值对 -- select idcountByCommunity resultTypemap SELECT c.community_name as name, !-- 别名name对应Service层的item.get(name) -- COUNT(i.in_out_record_id) as count !-- 别名count对应item.get(count) -- FROM in_out_record i LEFT JOIN community c ON i.community_id c.community_id !-- 关联社区表 -- WHERE c.community_name IS NOT NULL !-- 过滤空值 -- GROUP BY c.community_name !-- 按社区名称分组统计 -- ORDER BY count DESC !-- 按数量降序排列 -- /select /mapper三、开发过程中的易错点提醒新手必看易错点 1数据库字段名错误Unknown column c.name错误现象接口报错Unknown column c.name in field list原因SQL 中使用c.name但实际社区表的名称字段是community_name解决方案将 SQL 中的c.name改为c.community_name as name避坑技巧先在数据库工具中测试 SQL 语句使用as给字段起别名统一前后端字段名易错点 2空值导致前端报错Cannot read properties of undefined错误现象前端控制台报错Cannot read properties of undefined (reading sort)原因后端返回null而非空数组前端对null调用.sort()方法解决方案在 Service 层添加空值检查if (list null) { list new ArrayList(); // 空值时返回空数组 }避坑技巧所有集合类型的返回值都要做空值保护易错点 3端口被占用Port 8282 was already in use错误现象项目启动失败提示端口被占用解决方案查找并杀死占用端口的进程Windowsnetstat -ano | findstr :8282 # 查找PID taskkill /PID 进程ID /F # 杀死进程或修改application.yml中的端口server: port: 8283 # 改为未被占用的端口易错点 4Result 类导入冲突错误现象编译报错找不到符号方法ok原因MyBatis 的Result注解与自定义的Result类重名解决方案使用全类名指定自定义 Result 类return com.qcby.smartcommunity.util.Result.ok().put(data, data);易错点 5数据只有一条names 只有一个元素错误现象接口只返回一个社区的数据原因in_out_record表中所有记录的community_id都指向同一个社区解决方案检查数据录入逻辑确保community_id正确赋值手动更新测试数据-- 分配不同记录到不同社区 UPDATE in_out_record SET community_id 2 WHERE in_out_record_id IN (1,2,3); UPDATE in_out_record SET community_id 3 WHERE in_out_record_id IN (4,5);四、新手常见疑问解答疑问 1为什么要分 Controller/Service/Mapper 三层答三层架构是 Java 后端的最佳实践核心目的是解耦Controller只负责接收请求和返回响应不处理业务逻辑Service只处理业务逻辑不关心数据怎么查、请求怎么来Mapper只负责数据库操作不关心业务逻辑好处修改某一层的代码不会影响其他层比如换数据库只需改 Mapper改前端交互只需改 Controller。疑问 2Autowired 是什么为什么能直接用 InOutService答Autowired是 Spring 的依赖注入注解Spring 启动时会扫描带有Service、Controller、Mapper等注解的类自动创建这些类的对象称为 Bean并管理使用Autowired时Spring 会自动把对应的 Bean 注入到当前类中新手理解不用自己new InOutService()Spring 帮你创建并赋值。疑问 3为什么返回 Map 而不是自定义实体类答本例中返回结构简单只有两个数组用 Map 更灵活如果是复杂结构建议用实体类// 自定义实体类示例 public class ChartData { private ListString names; private ListInteger nums; // getter/setter }选择原则简单结构用 Map复杂结构用实体类可读性更高。疑问 4前端怎么调用这个接口答以 Vue 为例调用示例import axios from axios // 定义接口调用方法 export function getChartData() { return axios.get(/sys/inOut/chart) } // 在组件中使用 async mounted() { try { const res await getChartData() if (res.code 200) { const { names, nums } res.data // 渲染柱状图 this.renderChart(names, nums) } } catch (e) { console.error(接口调用失败, e) } }五、接口测试方法1.使用 Postman 测试请求方式GET请求地址http://localhost:8282/sys/inOut/chart预期响应包含names和nums数组的 JSON2.数据库验证直接执行 Mapper 中的 SQL 语句检查返回结果是否正确SELECT c.community_name as name, COUNT(i.in_out_record_id) as count FROM in_out_record i LEFT JOIN community c ON i.community_id c.community_id WHERE c.community_name IS NOT NULL GROUP BY c.community_name ORDER BY count DESC;总结核心开发流程分析文档 → 编写 Controller → 定义 Service 接口 → 实现 Service 逻辑 → 编写 Mapper 接口和 XML → 测试验证。关键避坑点字段名匹配、空值保护、端口冲突、类名冲突、数据分布。核心思想三层架构解耦统一响应格式前后端数据结构对齐。其实写接口并没有想象中那么难只要按固定的流程一步步来先理清需求再分层实现最后做好异常处理大部分问题都能迎刃而解。这次从无到有完成图表接口也让我更明白多动手、多排查、多总结比看再多教程都有用。希望这篇实战记录能帮你少走弯路也期待我们一起在实战里慢慢变稳、变强。博主后面还会坚持更新本项目各接口的开发流程感兴趣的小伙伴可以学习收藏~
AI智慧社区--从0到1开发柱状图数据接口
前言这段时间一直在做智慧社区项目之前完结的两篇已经把登录的整个流程和动态路由开发过程整理清楚有兴趣的小伙伴可以看小编本专栏的AI智慧社区开头的两篇文章。今天终于把首页的社区统计柱状图接口完整写完、测通了。从对着开发文档一点点理解需求到分层写代码、排查报错、再到前后端顺利联调整个过程踩了不少坑也实实在在学到了东西。我把完整的开发思路、每一步代码和自己踩过的易错点都整理出来既是给自己做一次复盘也希望能帮到和我一样正在学习 Spring Boot 接口开发的同学跟着流程就能从零写出一个能用、规范、不报错的图表接口。因为还有很多朋友是第一次看我的文章所以博主还是坚持老习惯上来先带大家捋顺对照接口开发的思路和本项目的技术栈老朋友可以直接看第二节开发步骤。一、接口开发核心思路1. 需求分析对照开发文档开发目标实现/sys/inOut/chart接口返回符合以下格式的 JSON 数据用于前端柱状图渲染{ msg: 操作成功, code: 200, data: { names: [社区1, 社区2, 社区3], // 社区名称数组 nums: [5, 3, 1] // 对应社区的统计数值 } }核心业务逻辑统计in_out_record表中各社区的出入记录数量关联community表获取社区名称。2.技术栈与核心术语解释技术 / 术语通俗解释作用Spring Boot简化 Spring 开发的框架快速搭建 Java 后端项目自动配置 Tomcat、MyBatis 等MyBatis持久层框架实现 Java 对象与数据库的映射简化 SQL 操作Controller控制器接收前端请求调用 Service 层返回响应数据Service/ServiceImpl服务层处理业务逻辑Controller 和 Mapper 的中间层Mapper/XML数据访问层定义数据库操作接口XML 编写具体 SQLResult 封装类统一响应格式规范接口返回值包含 code/msg/dataRESTful API接口设计风格通过 URLHTTP 方法定义接口本例用 GET 请求二、开发步骤一步步教你写1.创建 Controller 层接收请求核心作用作为前端与后端的入口接收 HTTP 请求调用 Service 层处理业务最终返回格式化响应。package com.qcby.smartcommunity.controller; import com.qcby.smartcommunity.service.InOutService; import com.qcby.smartcommunity.util.Result; // 统一响应封装类 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; import java.util.Map; /** * 出入记录图表数据接口控制器 * RestController组合注解 Controller ResponseBody返回JSON而非页面 * RequestMapping定义接口根路径 /sys/inOut */ RestController RequestMapping(/sys/inOut) public class InOutController { // 自动注入Service层对象依赖注入 Autowired private InOutService inOutService; /** * 柱状图数据接口 * GetMapping指定GET请求接口路径 /chart * return 统一格式的响应结果 */ GetMapping(/chart) public Result getChart() { // 调用Service层获取数据 MapString, Object data inOutService.getChartData(); // 封装成统一响应格式返回Result.ok()表示成功put添加数据 return Result.ok().put(data, data); } }2.创建 Service 接口定义业务方法核心作用定义业务逻辑的抽象方法解耦 Controller 与具体实现。package com.qcby.smartcommunity.service; import java.util.Map; /** * 出入记录服务接口 * 定义业务方法具体实现在ServiceImpl中 */ public interface InOutService { /** * 获取图表数据 * return 包含names和nums的Map */ MapString, Object getChartData(); }3.创建 Service 实现类处理业务逻辑核心作用实现 Service 接口调用 Mapper 层查询数据库处理数据格式转换。package com.qcby.smartcommunity.service.impl; import com.qcby.smartcommunity.mapper.InOutMapper; import com.qcby.smartcommunity.service.InOutService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Service接口的具体实现 * Service标记为Spring服务组件让Spring管理 */ Service public class InOutServiceImpl implements InOutService { // 注入Mapper层对象 Autowired private InOutMapper inOutMapper; Override public MapString, Object getChartData() { // 1. 调用Mapper查询数据库获取社区名称和对应数量 ListMapString, Object list inOutMapper.countByCommunity(); // 2. 空值保护避免null导致前端报错 if (list null) { list new ArrayList(); } // 3. 数据格式转换适配前端要求 ListString names new ArrayList(); // 社区名称数组 ListInteger nums new ArrayList(); // 数量数组 for (MapString, Object item : list) { // 从查询结果中获取社区名称和数量 names.add((String) item.get(name)); nums.add(((Number) item.get(count)).intValue()); } // 4. 封装返回数据 MapString, Object result new HashMap(); result.put(names, names); result.put(nums, nums); return result; } }4.创建 Mapper 接口定义数据库操作核心作用定义数据库查询方法MyBatis 通过动态代理生成实现类。package com.qcby.smartcommunity.mapper; import org.apache.ibatis.annotations.Mapper; import java.util.List; import java.util.Map; /** * 出入记录数据访问接口 * Mapper标记为MyBatis的Mapper接口让MyBatis扫描并生成代理类 */ Mapper public interface InOutMapper { /** * 按社区统计出入记录数量 * return 包含name(社区名)和count(数量)的Map列表 */ ListMapString, Object countByCommunity(); }5.编写 Mapper XML实现 SQL 查询核心作用编写具体的 SQL 语句实现数据库查询。?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd !-- namespace必须与Mapper接口全类名一致 -- mapper namespacecom.qcby.smartcommunity.mapper.InOutMapper !-- id与Mapper接口中的方法名一致 resultType返回类型map表示返回键值对 -- select idcountByCommunity resultTypemap SELECT c.community_name as name, !-- 别名name对应Service层的item.get(name) -- COUNT(i.in_out_record_id) as count !-- 别名count对应item.get(count) -- FROM in_out_record i LEFT JOIN community c ON i.community_id c.community_id !-- 关联社区表 -- WHERE c.community_name IS NOT NULL !-- 过滤空值 -- GROUP BY c.community_name !-- 按社区名称分组统计 -- ORDER BY count DESC !-- 按数量降序排列 -- /select /mapper三、开发过程中的易错点提醒新手必看易错点 1数据库字段名错误Unknown column c.name错误现象接口报错Unknown column c.name in field list原因SQL 中使用c.name但实际社区表的名称字段是community_name解决方案将 SQL 中的c.name改为c.community_name as name避坑技巧先在数据库工具中测试 SQL 语句使用as给字段起别名统一前后端字段名易错点 2空值导致前端报错Cannot read properties of undefined错误现象前端控制台报错Cannot read properties of undefined (reading sort)原因后端返回null而非空数组前端对null调用.sort()方法解决方案在 Service 层添加空值检查if (list null) { list new ArrayList(); // 空值时返回空数组 }避坑技巧所有集合类型的返回值都要做空值保护易错点 3端口被占用Port 8282 was already in use错误现象项目启动失败提示端口被占用解决方案查找并杀死占用端口的进程Windowsnetstat -ano | findstr :8282 # 查找PID taskkill /PID 进程ID /F # 杀死进程或修改application.yml中的端口server: port: 8283 # 改为未被占用的端口易错点 4Result 类导入冲突错误现象编译报错找不到符号方法ok原因MyBatis 的Result注解与自定义的Result类重名解决方案使用全类名指定自定义 Result 类return com.qcby.smartcommunity.util.Result.ok().put(data, data);易错点 5数据只有一条names 只有一个元素错误现象接口只返回一个社区的数据原因in_out_record表中所有记录的community_id都指向同一个社区解决方案检查数据录入逻辑确保community_id正确赋值手动更新测试数据-- 分配不同记录到不同社区 UPDATE in_out_record SET community_id 2 WHERE in_out_record_id IN (1,2,3); UPDATE in_out_record SET community_id 3 WHERE in_out_record_id IN (4,5);四、新手常见疑问解答疑问 1为什么要分 Controller/Service/Mapper 三层答三层架构是 Java 后端的最佳实践核心目的是解耦Controller只负责接收请求和返回响应不处理业务逻辑Service只处理业务逻辑不关心数据怎么查、请求怎么来Mapper只负责数据库操作不关心业务逻辑好处修改某一层的代码不会影响其他层比如换数据库只需改 Mapper改前端交互只需改 Controller。疑问 2Autowired 是什么为什么能直接用 InOutService答Autowired是 Spring 的依赖注入注解Spring 启动时会扫描带有Service、Controller、Mapper等注解的类自动创建这些类的对象称为 Bean并管理使用Autowired时Spring 会自动把对应的 Bean 注入到当前类中新手理解不用自己new InOutService()Spring 帮你创建并赋值。疑问 3为什么返回 Map 而不是自定义实体类答本例中返回结构简单只有两个数组用 Map 更灵活如果是复杂结构建议用实体类// 自定义实体类示例 public class ChartData { private ListString names; private ListInteger nums; // getter/setter }选择原则简单结构用 Map复杂结构用实体类可读性更高。疑问 4前端怎么调用这个接口答以 Vue 为例调用示例import axios from axios // 定义接口调用方法 export function getChartData() { return axios.get(/sys/inOut/chart) } // 在组件中使用 async mounted() { try { const res await getChartData() if (res.code 200) { const { names, nums } res.data // 渲染柱状图 this.renderChart(names, nums) } } catch (e) { console.error(接口调用失败, e) } }五、接口测试方法1.使用 Postman 测试请求方式GET请求地址http://localhost:8282/sys/inOut/chart预期响应包含names和nums数组的 JSON2.数据库验证直接执行 Mapper 中的 SQL 语句检查返回结果是否正确SELECT c.community_name as name, COUNT(i.in_out_record_id) as count FROM in_out_record i LEFT JOIN community c ON i.community_id c.community_id WHERE c.community_name IS NOT NULL GROUP BY c.community_name ORDER BY count DESC;总结核心开发流程分析文档 → 编写 Controller → 定义 Service 接口 → 实现 Service 逻辑 → 编写 Mapper 接口和 XML → 测试验证。关键避坑点字段名匹配、空值保护、端口冲突、类名冲突、数据分布。核心思想三层架构解耦统一响应格式前后端数据结构对齐。其实写接口并没有想象中那么难只要按固定的流程一步步来先理清需求再分层实现最后做好异常处理大部分问题都能迎刃而解。这次从无到有完成图表接口也让我更明白多动手、多排查、多总结比看再多教程都有用。希望这篇实战记录能帮你少走弯路也期待我们一起在实战里慢慢变稳、变强。博主后面还会坚持更新本项目各接口的开发流程感兴趣的小伙伴可以学习收藏~