前言

        这段时间一直在做智慧社区项目,之前完结的两篇已经把登录的整个流程和动态路由开发过程整理清楚,有兴趣的小伙伴可以看小编本专栏的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 编写具体 SQL
Result 封装类 统一响应格式 规范接口返回值(包含 code/msg/data)
RESTful API 接口设计风格 通过 URL+HTTP 方法定义接口(本例用 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层获取数据
        Map<String, 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
     */
    Map<String, 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 Map<String, Object> getChartData() {
        // 1. 调用Mapper查询数据库(获取社区名称和对应数量)
        List<Map<String, Object>> list = inOutMapper.countByCommunity();
        
        // 2. 空值保护(避免null导致前端报错)
        if (list == null) {
            list = new ArrayList<>();
        }

        // 3. 数据格式转换(适配前端要求)
        List<String> names = new ArrayList<>();  // 社区名称数组
        List<Integer> nums = new ArrayList<>();   // 数量数组
        
        for (Map<String, Object> item : list) {
            // 从查询结果中获取社区名称和数量
            names.add((String) item.get("name"));
            nums.add(((Number) item.get("count")).intValue());
        }

        // 4. 封装返回数据
        Map<String, 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列表
     */
    List<Map<String, Object>> countByCommunity();
}

5.编写 Mapper XML(实现 SQL 查询)

核心作用:编写具体的 SQL 语句,实现数据库查询。

<?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">
<!-- namespace必须与Mapper接口全类名一致 -->
<mapper namespace="com.qcby.smartcommunity.mapper.InOutMapper">

    <!-- 
        id:与Mapper接口中的方法名一致
        resultType:返回类型(map表示返回键值对)
     -->
    <select id="countByCommunity" resultType="map">
        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)

  • 错误现象:项目启动失败,提示端口被占用
  • 解决方案查找并杀死占用端口的进程(Windows):
netstat -ano | findstr :8282  # 查找PID
taskkill /PID 进程ID /F       # 杀死进程

或修改 application.yml 中的端口

server:
  port: 8283  # 改为未被占用的端口

易错点 4:Result 类导入冲突

  • 错误现象:编译报错找不到符号方法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。

疑问 2:@Autowired 是什么?为什么能直接用 InOutService?

@Autowired 是 Spring 的依赖注入注解:

  • Spring 启动时会扫描带有 @Service@Controller@Mapper 等注解的类
  • 自动创建这些类的对象(称为 Bean)并管理
  • 使用 @Autowired 时,Spring 会自动把对应的 Bean 注入到当前类中

新手理解:不用自己 new InOutService(),Spring 帮你创建并赋值。

疑问 3:为什么返回 Map 而不是自定义实体类?

:本例中返回结构简单(只有两个数组),用 Map 更灵活;如果是复杂结构,建议用实体类:

// 自定义实体类示例
public class ChartData {
    private List<String> names;
    private List<Integer> 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
  • 预期响应:包含 namesnums 数组的 JSON

2.数据库验证

        直接执行 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 → 测试验证。
  • 关键避坑点:字段名匹配、空值保护、端口冲突、类名冲突、数据分布。
  • 核心思想:三层架构解耦,统一响应格式,前后端数据结构对齐。

        其实写接口并没有想象中那么难,只要按固定的流程一步步来,先理清需求,再分层实现,最后做好异常处理,大部分问题都能迎刃而解。这次从无到有完成图表接口,也让我更明白:多动手、多排查、多总结,比看再多教程都有用。希望这篇实战记录,能帮你少走弯路,也期待我们一起在实战里慢慢变稳、变强。

         博主后面还会坚持更新本项目各接口的开发流程,感兴趣的小伙伴可以学习收藏~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐