1. 项目概述与需求拆解
1.1 课后延时服务系统的核心业务场景
课后延时服务这几年在各地中小学全面铺开,核心业务不复杂,但细节特别多。简单说就是:学校在下午正常放学之后,提供一段额外的托管时间,学生可以留在学校写作业、参加兴趣社团、体育训练或者自习,老师轮流值班看管。这个系统就是把这套线下流程搬到线上,让家长能选课、能缴费、能请假,让老师能点考勤、能记录表现,让管理员能排班、能统计费用。
我在一个模拟项目中完整开发过这样一套系统,技术栈就是标题里写的 Java + SpringBoot + SSM 这套组合。整套系统做下来,我的感受是:技术本身不复杂,真正的复杂度全在业务细节里——一个课时怎么算钱、请假怎么退费、多个社团冲突怎么处理、费用结算周期怎么定,这些才是让开发工作量翻倍的地方。
标题里提到“源码+LW+调试文档+讲解”,这是目前高校毕业设计和课程项目里非常典型的一种交付形式。LW是论文/文档的缩写,调试文档和讲解则是为了帮助接手的人真正跑起来、能说清楚原理。所以这篇文章我不只讲技术选型,更重要的是把整个项目的设计思路、表结构、权限模型、关键代码实现和排查经验都过一遍,无论你是要做毕业设计还是想自己搭建一套课后服务管理系统,都能直接参考。
提示:本文所有表名、类名、代码片段均来自我实际开发过的模拟项目,你可以直接抄走改改字段名就能用。
1.2 角色权限与核心业务流程
课后延时服务系统涉及三类角色,这是所有业务设计的起点。
管理员(学校教务人员):负责创建学期、设置可选的课后服务项目(如作业辅导、篮球社团、书法兴趣班)、给每个项目分配授课教师和时间场地、审核家长提交的报名、处理退费申请、查看全校的考勤统计和费用汇总。
教师:查看自己负责的课程班级名单,上课前点名,课后补充学生的课堂表现备注,月末确认自己的课时费统计。
家长/学生:查看学校发布的课后服务课程列表,帮孩子选课报名,在线支付课时费,查看孩子的考勤记录,提交请假申请。
整个核心业务流程是一条线:学校发布课程 → 家长在线选课缴费 → 管理员审核 → 教师开课点名 → 月末结算课时费 → 学期结束处理退费或结转。
搞清楚这条线,后面做数据库设计和接口设计就有了底气。很多新手上来就写代码,写到最后发现表对不上、状态流转不清,就是因为没有先把业务链路理顺。
2. 技术选型与架构设计
2.1 为什么是SpringBoot + SSM这个组合
标题里写的是“Java+SpringBoot+SSM”,这其实是两代技术方案的融合。SSM是 Spring + SpringMVC + MyBatis 三件套,早期Java Web开发的主流组合;SpringBoot 则是后来出现的“约定大于配置”的快速开发框架。很多课程项目写成“SpringBoot+SSM”,实际含义是:用SpringBoot做核心容器和自动配置,用SpringMVC处理Web请求,用MyBatis做数据库持久层。
这套组合的好处非常明显。第一,SpringBoot内置了Tomcat,不需要单独部署WAR包到外部容器,一个java -jar就能启动,这对要写部署文档、给老师演示的项目来说太友好了。第二,SSM这套体系资源多、资料全,遇到任何报错都能搜到解决方案,不像某些小众框架出了问题没人接招。第三,MyBatis写SQL是手动的,对业务复杂的场景反而更灵活——课后服务系统的统计报表SQL往往比较复杂,用MyBatis可以精确控制每一条查询语句。
我在项目里用的是 SpringBoot 2.7.x + MyBatis + SpringMVC,JDK用的 1.8,数据库 MySQL 8.0,前端模板引擎用 Thymeleaf,配合 Bootstrap + jQuery 做页面交互。这套组合的好处是:不需要额外搭前端工程,不用纠结跨域问题,一个工程就能把后台管理页面全部搞定,非常适合中小型教务管理类系统。
2.2 单体架构还是前后端分离
这个问题我在一开始也纠结过。现在很多项目都推前后端分离,Vue + SpringBoot 的组合。但课后延时服务系统这种教务管理类项目,我最终选了单体架构 + 服务端渲染,原因是:
- 系统用户量级不大,一个学校几千个学生,选课高峰期的并发也就几十到几百,单体架构完全扛得住。
- 页面数量不多但每个页面交互细节多,用 Thymeleaf 在服务端渲染页面,开发速度最快,改起来也直观。
- 前后端分离需要维护两套工程、处理鉴权(JWT或Session跨域)、联调接口,对单人开发来说成本翻倍。
如果要做毕业设计,我强烈建议就用单体架构。答辩时你能把整个请求链路说清楚——从浏览器发起请求到控制器、服务层、Mapper层、数据库,一条线讲明白,比拆成前后端分离后讲不清接口联调细节要好得多。
注意:如果你未来要往行业里去,课后服务系统有可能会扩展成区域级平台,那种场景才需要考虑前后端分离和微服务。做一个学校级别的系统,单体架构是最务实的方案。
2.3 三层架构与包结构设计
后端代码我按经典的三层架构组织,包结构如下:
com.example.afterschool ├── controller // 控制层,接收请求,返回页面或JSON │ ├── AdminController.java │ ├── TeacherController.java │ └── ParentController.java ├── service // 业务层,核心逻辑 │ ├── CourseService.java │ ├── EnrollmentService.java │ ├── AttendanceService.java │ └── PaymentService.java ├── mapper // 持久层接口,对应Mapper.xml │ ├── CourseMapper.java │ ├── EnrollmentMapper.java │ └── ... ├── entity // 数据库实体类(POJO) │ ├── Course.java │ ├── Student.java │ └── ... ├── dto // 数据传输对象,用于参数校验与装配 │ ├── CourseQueryDTO.java │ └── ... ├── config // 配置类,拦截器、WebMvc配置 │ ├── WebMvcConfig.java │ └── ... ├── interceptor // 登录拦截器、角色拦截器 └── utils // 工具类,如日期处理、费用计算这种分层的好处是各层职责清晰:Controller只负责参数接收和视图渲染,Service写业务逻辑,Mapper只做数据存取。我见过很多新手项目把SQL写在Controller里面,做是能做出来,但一旦需求变化——比如退费逻辑调整——你就得在十几个地方同步修改,维护成本特别高。
3. 数据库设计与表结构拆解
3.1 核心数据表全景
课后延时服务系统的核心表,我按业务模块拆成五个部分:用户与权限、课程与班级、选课与缴费、考勤与表现、学期与配置。
完整表清单如下:
| 模块 | 表名 | 用途 |
|---|---|---|
| 学期配置 | semester | 学期信息,如2025学年上学期 |
| 课程管理 | course | 课后服务课程/项目,含课程名、时段、费用标准 |
| 班级管理 | course_class | 课程下按学生人数拆分的班级,含时间场次 |
| 教师管理 | teacher | 教师基础信息,关联用户表 |
| 学生管理 | student | 学生基础信息,关联家长账号 |
| 家长管理 | parent | 家长账号信息 |
| 选课报名 | enrollment | 家长为学生选择的课程报名记录 |
| 缴费记录 | payment | 每门课程的费用流水 |
| 考勤记录 | attendance | 每次上课的学生出勤状态 |
| 请假记录 | leave_request | 学生请假申请与审批状态 |
| 课程评价 | course_comment | 教师课后对学生的表现备注 |
| 系统用户 | sys_user | 统一账号表,区分管理员/教师/家长角色 |
这里有一个核心设计点:用户统一表和角色分离。我用一张sys_user表存放账号信息(用户名、加密密码、角色类型),而教师、学生、家长的详细信息分别用单独的表存储,通过user_id关联。这样登录认证只需要验证一张表,业务数据又不会混在一起。
3.2 选课报名表的状态机设计
选课报名表enrollment是系统的核心,它的状态字段设计直接影响后续所有业务流程。我最终设计了五个状态:
PENDING(待审核)——家长提交选课后,还没被管理员确认APPROVED(已通过)——管理员审核通过,课程生效REJECTED(已驳回)——管理员驳回报名,费用原路退回CANCELLED(已取消)——家长开课前取消,费用退还FINISHED(已完成)——学期结束,课程正常结课
这五个状态基本覆盖了所有业务流转。细节在于:状态流转是有方向限制的,比如CANCELLED只能从PENDING或APPROVED转换,已经是FINISHED的报名就不能再取消。这个逻辑必须在 Service 层做校验,不能在 Mapper 层放任不管。
费用相关的字段我也做了充分考虑。enrollment表里存total_amount(总金额)和refund_amount(已退金额),每次退款操作都会同时更新这两个字段,避免月末结算时需要临时做聚合查询。
3.3 MyBatis动态SQL的实战写法
课后服务系统的查询条件往往不固定——管理员要按学期过滤、按课程类型过滤、按年级过滤、按状态过滤,用 MyBatis 的动态 SQL 写起来非常方便。这里贴一段我在课程列表查询时写的 Mapper 映射:
<select id="selectCoursePage" resultType="com.example.afterschool.entity.Course"> SELECT c.id, c.course_name, c.course_type, c.semester_id, c.total_hours, c.unit_price, c.max_student, c.status, t.teacher_name, ct.week_day, ct.start_time, ct.end_time FROM course c LEFT JOIN teacher t ON c.teacher_id = t.id LEFT JOIN course_time ct ON ct.course_id = c.id <where> <if test="semesterId != null"> AND c.semester_id = #{semesterId} </if> <if test="courseType != null and courseType != ''"> AND c.course_type = #{courseType} </if> <if test="grade != null and grade != ''"> AND c.grade = #{grade} </if> <if test="status != null and status != ''"> AND c.status = #{status} </if> </where> ORDER BY c.create_time DESC </select>这种写法最舒服的地方就是想查哪个条件就传哪个条件,不用写多个 SQL 分支。但这个方案有个前提:MyBatis 的if判断需要在 Service 层把参数封装好,千万别把查询条件直接塞给 Mapper 层做拼接。
另一个容易踩的坑是分页。我用了 PageHelper 插件,但实测发现 PageHelper 和复杂嵌套查询可能冲突,一个子查询中如果出现ORDER BY字段不在主表时,PageHelper 的自动 count 语句会出错。解决方案有两种:自己写 count 语句,或者在做分页时不要传太复杂的关联条件。我的做法是能不加关联就不加,把关联字段冗余存储到主表。
4. 核心业务功能与关键代码实现
4.1 登录认证与角色权限控制
课后服务系统的登录认证我用了最简单的 Session 机制,SpringBoot 架构里配一个拦截器就解决了。权限控制的核心点在于:不同角色能访问的接口和页面必须严格隔离。
我实现了三层拦截策略:
- 第一层:登录拦截——所有未登录的请求一律跳转到登录页。
- 第二层:角色校验——通过注解
@RequiresRole("admin")标识接口需要的角色权限。 - 第三层:数据权限——教师只能看自己班级的数据,家长只能看自己孩子的数据,这些在 Service 层做数据过滤。
拦截器实现的核心逻辑如下:
@Component public class RoleInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果不是方法上的注解,直接放行 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; RequiresRole requiresRole = handlerMethod.getMethodAnnotation(RequiresRole.class); if (requiresRole == null) { return true; } // 从Session中取当前登录用户 SysUser loginUser = (SysUser) request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("/login.html"); return false; } // 校验角色必须匹配 if (!requiresRole.value().equals(loginUser.getRoleType())) { response.setStatus(HttpStatus.FORBIDDEN.value()); response.getWriter().write("{\"code\":403,\"msg\":\"权限不足\"}"); return false; } return true; } }实际开发中我建议你把角色校验做成注解 + Redis 缓存用户信息的组合。因为每次校验都查库效率不高,Session 里可以缓存基本信息,角色信息也可以缓存到 Session 或 Redis 中。对于在学校这种内网环境下运行的系统,Session 机制已经足够了。
密码加密这块也是一个重点。我用的 BCrypt 加密,不是 MD5。MD5 现在早就被蹿字典了,BCrypt 每次生成的哈希都不一样,防彩虹表效果好很多。Spring Security 的BCryptPasswordEncoder单独拿出来用即可,不需要把整个 Security 框架引入进来。
4.2 选课报名与费用计算
选课报名是课后服务系统最关键、最容易出问题的模块。核心逻辑包含三个步骤:
- 校验阶段:课程是否存在且状态为“报名中”,学生是否符合选课条件(年级匹配),学生是否已经选过该课程,课程班级是否还有名额。
- 计算费用:按课时单价和总课时数计算应收金额,如果是按月缴费则按当月剩余课时比例折算。
- 写库阶段:插入报名记录和缴费记录。
这里最需要谨慎的是并发问题。多个家长同时给同一个孩子抢同一个热门课时,如果两个请求同时通过名额校验,就会出现超报。我在模拟项目里用了两种方案来解决:
第一种是乐观锁。在course_class表里加一个version字段,更新时带上版本号作为条件:
UPDATE course_class SET selected_count = selected_count + 1, version = version + 1 WHERE id = #{classId} AND selected_count < max_student AND version = #{version}如果更新影响行数为 0,说明数学被占满了或者版本冲突,就返回报名失败。
第二种是数据库层面的唯一约束。我在enrollment表加了UNIQUE KEY uk_student_course (student_id, course_id),避免同一个人重复报名同一门课。
费用计算的业务规则也要考虑清楚。我采用的是“按课时计费”模式,总费用 = 单价 × 总课时数。但请假情况会影响到后续退费:如果学生请了长假,超过一周以上,管理员可以通过退费流程按剩余课时退款,这个退款金额正确计算的关键在于考勤表里记录了学生的实际上课情况。
4.3 考勤管理与数据统计
考勤模块我采用“一键点名+手动调整”的模式。教师打开班级点名页面时,系统默认把该班级所有已报名学生标记为“出勤”,教师只需要点击缺勤的学生即可,这个设计对实际使用非常友好——默认情况比挨个点名快得多。
考勤表attendance关键字段:
id, class_id, student_id, course_date, week_day, status(0缺席/1出勤/2请假/3迟到), remark值得一提的设计是:考勤记录在课程开始前由系统自动初始化生成。也就是说,管理员在学期初创建课程时,系统根据课程时间表自动生成整学期每一天该班级的考勤记录占位数据,状态默认为“未点名”。这样做的最大好处是:教师端可以看到整学期的考勤格子,不会出现忘了补上某一天记录的情况,而且后续统计报表(全勤率、出勤天数)可以直接基于考勤表查询,不会漏数据。
数据统计我写了三个维度的 SQL:
- 按课程统计:每门课程的报名人数、实到人数、出勤率
- 按班级统计:每个班级的平均出勤率,用于对比各班情况
- 按教师统计:每位教师负责课程的总课时数、应结算课时费
结算课时费的 SQL 只统计“已正常结束”的课程,请假、缺席都不计入课时费结算。我当时踩过一个坑:因为把占位考勤也生成了,统计报表时如果不加状态过滤,会导致未开始的课程也被算进结算数据里。所以你的统计逻辑务必要加上course_date < NOW()和status = 1的条件。
4.4 请假与退费流程
请假和退费是业务闭环的最后一环,这块逻辑的复杂度超乎很多人想象。
家长提交请假申请后,流程是:家长端提交请假(选择请假时间范围和原因)→ 系统自动将该时段的考勤状态置为“请假” → 教师端确认 → 管理员审核,如果属于长期请假(超过一周)则触发退费计算。
退费计算的公式我这样定的:
退款金额 = 该课程剩余课时数 × 课时单价 - 已产生的手续费(如10元)剩余课时数的计算方法是:先查该课程的总课时数,然后统计该学生已经上过的课时数(考勤表中状态为“出勤”的记录数),两者相减。这个逻辑说起来简单,但要注意一个边界情况:如果学生已经把整学期课程都上完了,但最后一周才提交请假,剩余课时也可能为 0,系统就不能退款。
我在项目中单独写了一个退费服务类:
@Service public class RefundService { /** * 计算可退金额 */ public BigDecimal calculateRefund(Enrollment enrollment) { // 已购买总课时 = 课程总课时 Integer totalHours = enrollment.getTotalHours(); // 已出勤课时 = 考勤表中状态为出勤的课时数 Integer attendedHours = attendanceMapper.countAttended( enrollment.getStudentId(), enrollment.getCourseId()); // 剩余课时 = 总课时 - 已出勤课时 int remainHours = totalHours - attendedHours; if (remainHours <= 0) { return BigDecimal.ZERO; } // 退款金额 = 剩余课时 x 单价 - 手续费 BigDecimal refund = enrollment.getUnitPrice() .multiply(BigDecimal.valueOf(remainHours)) .subtract(BigDecimal.valueOf(10)); return refund.max(BigDecimal.ZERO); } }这个类是支付模块的核心,也最容易出问题。BigDecimal 的减法结果可能为负,所以最后一定用max(BigDecimal.ZERO)做兜底。这些都是实测踩坑出来的,写一遍记牢,以后做任何计费类系统都能直接用。
5. 部署环境与调试文档要点
5.1 本地运行环境配置
这套系统要跑起来,需要准备的环境如下:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 我用的是 1.8,兼容性最稳 |
| MySQL | 8.0 | 5.7 也可以,但 8.0 用起来更顺手 |
| Maven | 3.6+ | 依赖管理 |
| Tomcat | SpringBoot 内置 | 无需单独安装 |
启动流程很简单:先建库,把sql/afterschool.sql脚本导入,然后在application.yml里改数据库账号密码,最后运行主启动类即可。
Linux 服务器上部署时有个细节需要注意:SpringBoot 的 Jar 包运行时,MyBatis 的 Mapper XML 文件需要放在resources/mapper目录下,并且pom.xml里要确保构建时能把这部分资源打进去。很多人部署到 Linux 上发现找不到 Mapper 映射文件,就是因为资源的filtering配置把 XML 过滤掉了。
5.2 调试文档的核心思路
调试文档不是把代码抄一遍,而是要让接手的人能独立排查问题。我按这个结构组织调试文档:
- 环境配置部分:JDK、Maven、MySQL 的安装,数据库脚本导入,控制台启动日志
- 常见启动报错及解决:端口被占用、数据库连接失败、Redis 未启动、Mapper 文件打包缺失
- 业务调试指南:如何模拟一次完整的选课流程,如何构造测试数据
- 排查脚本:贴出几条有用的 SQL,快速查看数据状态
我建议你写调试文档时一定要带上测试数据,否则接手的人很难验证功能。我往数据库里预置了十个以上的虚拟学生、三个课程,每个课程都保证有报名的记录,这样一打开系统就能看到真实效果,不用费劲去造数。
调试文档里我还加了一个排查经验表,对应实际踩过的坑,这个在后面对应章节详细展开。
6. 常见问题与排查技巧实录
6.1 启动阶段高发问题及处理
问题一:SpringBoot 启动时报端口被占用。
处理方法很简单,先确认是否此前有残留进程,Windows 上用netstat -ano | findstr 8080找到 PID,任务管理器结束进程。如果项目同时起了多个实例也会出现。开发环境建议在application.yml里加server.port=8080一步配死。
注意:很多同学启动后不知道系统是否真的跑起来了,直接看控制台最后的
Started ... in x seconds日志才是准的,别只看有没有报错就完事。
问题二:访问页面时白屏或者 404。
这类问题 80% 是静态资源路径配错了。Thymeleaf 模板放在templates目录下,静态资源(CSS、JS)放在static目录下,Controller 返回的字符串要和模板文件名对应。排查时打开浏览器的 F12 开发者工具,查看具体哪个资源返回 404。
问题三:MyBatis 报Invalid bound statement (not found)。
这是因为 Mapper 接口和 XML 没有正确关联。检查三处:
- XML 文件是否在
resources/mapper目录下 application.yml中mybatis.mapper-locations配置是否正确MapperScan注解的扫描包路径是否准确
提醒一下:这三处任何一个不对,就会出现这种报错,排查顺序也按这个来。
6.2 业务逻辑层高发问题及处理
问题一:选课报名超员,两个家长同时选最后一个名额。
处理方式前面说过了,用乐观锁 UPDATE,更新影响行数为 0 则提示“名额不足”。我在测试环境用 JMeter 同时发 100 个请求验证过,加锁之前出现 3 个超额报名,加锁之后完全正常。
问题二:考勤日期不对,教师点名时看不到今天的学生名单。
这是日期格式和时区的问题。MySQL 的datetime类型和 Java 的LocalDate比对时,要保证数据库连接参数serverTimezone=Asia/Shanghai,否则会出现时区偏移 8 小时的问题。我当时本地一切正常,部署到云服务器上才发现这个坑,排查了半天。
问题三:费用统计汇总不准确。
这个问题的根源通常是报名状态没有过滤。统计报名人数时,必须带上status = 'APPROVED'条件,把待审核和已取消的排除掉。我在项目里做了一个统一过滤的查询工具方法,所有统计都走同一个入口,从源头避免口径不一致。
6.3 性能优化与安全加固
虽然学校系统并发不高,但我还是做了一些基础优化:
- 数据库索引:
enrollment表的student_id、course_id两个外键字段必须建索引,考勤表的course_date也要建索引,否则数据量大后统计查询会非常慢。 - 查询缓存:课程列表这种变化不频繁的数据可以加 Spring Cache,用
@Cacheable注解做本地缓存,减少重复查库。 - SQL 注入防护:所有查询都用
#{}占位符,千万别用${}拼接字符串,这是底线。 - XSS 防护:对输入框里的内容做转义处理,特别是课程名称、评语这类用户输入字段。
安全这块我单独强调一句:任何互联网暴露的系统,密码必须加密,接口必须有权限验证,这就是基本底线。
7. 项目文档与后续扩展建议
7.1 LW(设计文档)的写作思路
很多人做设计文档的时候头大,其实整套文档的逻辑主线就是:从需求出发,经过设计与实现,最后到验证与总结。
我写文档时的章节结构是这样的:
- 绪论:课后延时服务的背景意义,国内外现状
- 需求分析:角色分析、业务流程、用例图、功能需求与非功能需求
- 系统设计:架构设计、功能模块设计、数据库表结构设计
- 系统实现:按功能模块展示核心代码,并配关键代码说明
- 系统测试:功能测试用例设计、测试过程、测试结果
文档里可以贴代码片段,但不要大段大段把整个项目代码复制进去,重点是结合你自己的实现讲解设计思路。答辩老师最常问的问题是“为什么这么设计”,你如果能用“因为课后服务课程有周期性,所以我把学期和课程分成两表”这种基于业务逻辑的回答,比背任何源码都有效。
7.2 从学校级系统到区域级平台的扩展路径
如果你做完这套系统还有精力,可以思考一下扩展方向。
当前做的是单学校架构,所有数据挂在同一个学号下。未来如果要扩展成市/区级教育主管部门使用的平台,需要增加学校维度字段,所有核心表都加上school_id。支撑多个学校后,选课逻辑就要考虑跨校隔离、独立配置学期参数、统一数据上报接口。
数据接口上,可以和校园一卡通打通,同步考勤记录;可以和支付平台对接,实现自动对账;也可以接入短信通知,在选课审核通过、考勤异常时推送消息。
技术架构上,随着学校数量增多,单库单表的性能会逐渐吃紧。到时候可以把报名、缴费、考勤这类高频写入的业务拆分出来,逐步引入缓存、消息队列。这些扩展方向如果能在论文的“总结与展望”章节写出来,会显得你对系统的未来规划想得很透。
7.3 我对这套系统开发流程的整体体会
这套系统我前后从零开发到最终调试完毕,花了两周多时间。如果让我压缩一下流程,最有价值的经验排序是:
第一,先把业务规则写出来再写代码。我一开始直接建表写代码,结果到了请假退费环节被自己设计的表结构卡住了,不得不回头改表。后来我先把所有业务规则写成文字,再落到表和代码上,效率提升非常明显。
第二,项目文档和代码同步推进。很多人最后才补文档,结果文档写得和代码对不上,答辩一查就露馅。我建议每完成一个模块,就把这个模块的说明写进文档,最后整理起来非常快。
第三,测试环境要准备一批真实的业务数据。空数据系统跑起来看不出任何问题,但你也验证不了功能是否真正正确。我把一个学期的模拟数据全部造好,包括多个课程、几十个学生的选课记录、考勤记录、缴费记录,这样调试时一眼就能发现逻辑漏洞。
最后再分享一个小技巧:开发这类项目时,可以先把所有枚举状态(报名状态、请假状态、考勤状态)都定义成常量类,别用魔法字符串散落在代码里。我后来在几十处代码里统一替换状态值的时候,真的无比庆幸自己一开始就是这么设计的。