最近整理在线政务服务中心管理系统源码的时候一直在想一个问题这类系统市面上并不少为什么还要专门写一套后来把整个项目跑通、拆完、再重新部署一遍我意识到关键不在于有没有系统而在于业务建模和流程控制是否经得起真实业务场景的考验。下面这套基于SpringBootMyBatisMySQL后端、Vue前端的管理系统项目代号就叫nrlwabo我会把从需求拆解、数据建模、核心实现到部署上线的完整过程都讲一遍顺便把我踩过的坑也一并交代清楚。如果你正在做同类毕业设计、政务外包项目或者想从SSM迁移到SpringBootVue这套组合这篇文章应该能省下不少弯路。1. 业务分析在线政务服务中心到底在管什么1.1 从线下窗口到线上流转的业务场景还原政务服务中心的日常说白了就是两大核心对象的流转群众提交的事项以及窗口/后台对这些事项的处理。线下场景里群众需要取号、排队、提交纸质材料、等窗口人员手工录入再在多个部门之间来回跑。这中间最痛苦的不是业务本身而是状态不可见——群众不知道自己的材料卡在哪个环节窗口人员也不知道上一个环节的人处理到哪一步了。所以这套系统在设计时首先要回答的问题不是做什么界面好看而是如何把线下的流转动作变成线上的状态轨迹。系统需要支持的完整链路是这样的群众在线查看办事指南确认自己符合办理条件之后发起申请上传材料系统生成办件编号窗口人员收到办件后先做材料预审预审通过则正式受理受理后进入办理状态最后办结并开放评价入口。整个生命周期里每一步都有时间记录和操作人记录。1.2 系统角色与模块划分围绕着这条业务链路系统的角色和功能模块也就清晰了。我将角色划分为四类市民或企业用户、窗口人员、后台管理员、系统超级管理员。每个角色的权限边界不同这一点在后面权限设计部分还会单独展开。功能模块则分为前台与后台两大类。前台面向办事群众包括办事指南查询、事项分类浏览、在线申请、材料上传、预约时间、进度查询、办结评价后台面向管理人员和窗口人员包括用户管理、角色权限配置、事项维护、办件受理、材料审核、流程监控、通知公告发布、办件量和时效统计。这里有一个容易忽略的点事项本身需要配置材料清单和承诺办理时限因为每个事项的材料数量、类型都可能不一样如果系统里全部写死后面维护会非常痛苦。我的做法是为事项表单独设计材料配置字段前端动态渲染表单和上传组件。1.3 容易被忽略的业务细节真正接触过政务类项目的人都知道除了CRUD之外有几个业务细节特别容易在开发中被漏掉。第一个是办件编号的生成规则。政务场景里办件编号往往要求有业务含义通常包含区域编码、年份、事项编码和流水号而不是简单用自增主键。这个编号一旦对外公布就要保证唯一且可追溯。第二个是承诺时限和超时预警。每个事项都配置了办结时限天数系统在受理时要根据配置自动计算出承诺办结日期并用定时任务扫描即将到期或已超期的办件生成预警列表。这个功能虽然不复杂但它在真实使用中非常重要直接对应政务服务的考核指标。第三个是评价时效。办结之后要在规定时间内允许群众评价超时则默认关闭评价入口。如果不做这个控制用户随时可以回头评价半年前的办件数据统计就会失真。2. 技术选型复盘SpringBootVueMyBatisMySQL这套组合好在哪2.1 后端框架为什么选SpringBoot在后端框架的选择上可能有很多人犹豫过是继续用SSM三件套还是直接上SpringBoot或者干脆考虑Spring Cloud微服务。我的结论很直接单体应用且团队规模不大的情况下SpringBoot是性价比最高的选择。这背后的逻辑并不复杂。SSM时代最大的痛点是配置太繁琐一个XML配置里漏写一行启动时可以正常编译运行到某一个环节就莫名其妙报错。SpringBoot通过自动配置和starter机制把这些复杂度大幅收窄开发时只需要关注业务代码本身。它内嵌了Tomcat不需要单独部署WAR到外部容器一条java -jar命令就能跑起来这对部署和调试都是质的提升。政务服务中心这类系统的复杂度集中在业务状态流转和权限控制上并发量远没有到需要水平拆分的程度Spring Cloud那一套服务注册、配置中心、网关的复杂度在这个场景下反而不值得。2.2 MyBatis相比JPA和JDBC的取舍数据访问层的选型我曾经在MyBatis和Spring Data JPA之间反复比较过。JPA的优点是对于简单CRUD可以少写大量代码通过方法名推导SQL通过实体关系自动联表开发效率确实高。但这个项目我最终选了MyBatis原因也很现实政务类业务中复杂联表查询占比很高办件列表经常要关联用户表、事项表、窗口人员表、流转记录表而且查询条件因为业务变化会频繁调整这种场景下把SQL显式写在Mapper XML里维护起来更直接。MyBatis还有一个在政务项目里面很有用的能力动态SQL。比如办件列表页面的筛选条件——状态、申请时间、事项分类、办理人员——每个条件都有可能是空的用if标签组合条件比在Java代码里拼接SQL要干净得多。另外MyBatis的TypeHandler机制让枚举和数据库字段之间的转换变得简单例如状态字段在数据库里是数字在Java里对应一个状态枚举通过TypeHandler自动完成双向转换不用在每个查询和更新里手工set。2.3 前端采用Vue的核心理由前端选择Vue一方面是因为Vue本身的响应式数据和组件化开发模式非常适合管理后台这类界面另一方面是整个前端生态积累比较成熟Element UI这一套组件库把表格、表单、弹窗、分页这些高频组件全都覆盖了开发效率和UI统一性都有保障。特别是动态表单和状态步骤条这两个需求用Vue的组件化方式实现起来相当清晰。这里额外说一句Vue 2和Vue 3的选择。如果项目从零开始我建议直接考虑Vue 3配合Composition API在逻辑复用上的好处很明显但如果你手上这套系统是基于Vue 2的生态也不需要强行升级因为Element UI在Vue 2下依然稳定。关键是不要在Vue 2项目里引入Vue 3的写法也不要在一个项目里混用两套组件库版本一致性在前端工程里比什么都重要。2.4 数据库选型MySQL够用且有大量实践经验数据存储选MySQL在很多人看来可能不够高大上但结合这个系统的实际规模MySQL的InnoDB引擎在事务支持、行级锁、崩溃恢复方面都足够扎实运维成本又低社区经验丰富出问题时很容易找到解决方案。数据库连接串方面我建议使用MySQL 5.7或8.0版本并注意驱动类名在版本间的变化MySQL 8引入了新的驱动类com.mysql.cj.jdbc.Driver同时在连接串里建议显式配置useSSLfalse、serverTimezoneAsia/Shanghai、characterEncodingutf8mb4这几个参数不配好的话很容易碰到时区偏差和SSL连接报错后面会在部署章节详述。3. 数据库设计与后端核心架构3.1 核心表结构设计思路数据库设计是整个系统最需要花时间去想的部分。我把核心表划分为两组一组是系统支撑表一组是业务表。系统支撑表包括用户表、角色表、用户角色关联表、权限表业务表包括事项分类表、事项表、申请单表、申请材料表、预约表、流转记录表、评价表、通知公告表。以申请单表为例核心字段包括主键、办件编号、申请事项ID、申请用户ID、当前办理窗口人员ID、状态、提交时间、受理时间、承诺办结日期、实际办结时间、是否已评价以及逻辑删除标记。这里有两个字段值得专门说明承诺办结日期在受理时根据事项表的承诺时限实时计算出来状态字段只存一个数字具体的状态含义放在Java枚举里统一管理不在数据库里存中文避免因为改文案导致数据迁移。用户表这边要特别注意密码存储问题明文密码在任何场景下都是大忌我采用的是BCrypt加密存储即使数据库泄露原密码也很难被反向推出。用户表里还需要状态字段用于控制账号是否被锁定或停用这比直接删除用户记录更安全也保留了审计追溯的可能性。3.2 后端分层与统一响应结构后端工程按经典的controller、service、mapper三层划分实体类单独放在entity包里。controller层只负责接收请求参数和封装返回结果不写业务逻辑service层承载核心业务规则和事务控制mapper层负责SQL交互。为了让各层之间的返回格式统一我定义了一个Result类结构包含code、message、data三个字段成功时code为200失败时code为业务错误码前端统一通过code判断请求是否成功而不是依赖HTTP状态码。统一响应结构带来的好处在联调阶段就体现出来了。如果每个接口各自为政有的返回布尔值有的直接返回对象有的返回Map前端在axios拦截器里完全没法统一处理错误提示。有了Result类之后前端只需要判断code是否等于200不是则弹出message开发效率会提升非常多。3.3 接口路径设计与状态变更接口规范接口设计上我统一加了/api前缀并尽量遵循资源化命名例如用户管理用/api/admin/users事项查询用/api/items在线申请提交用/api/applications。这里要强调一个容易被做坏的点办件状态流转的接口设计。不少人的习惯是做一个通用接口比如PUT /api/applications/{id}前端把整个申请单对象都传过来后端接收后全字段更新。这种做法非常危险一旦前端异常覆盖了后端维护的字段业务数据就会出问题。我的做法是状态流转走专用接口每个动作对应一个独立端点例如提交申请、窗口预审、正式受理、办结归档。这些接口内部只做当前状态允许的变更只更新必要的字段其余字段不允许被覆盖。这个设计在后续讲权限控制时也会继续提到它是整个系统数据一致性的基础保障。3.4 全局异常处理与参数校验后端框架搭建时还有一个容易被忽视但极其重要的部分全局异常处理。如果不做统一处理代码里到处是try-catch不仅啰嗦还容易漏掉。我在common包下实现了GlobalExceptionHandler通过RestControllerAdvice统一捕获三类异常自定义的业务异常BizException直接返回业务提示参数校验异常MethodArgumentNotValidException把第一个失败字段的提示信息返回给前端兜底的Exception则返回通用错误信息并记录完整堆栈日志。参数校验我用的JSR 303注解方案实体字段上标注NotBlank、Size这些注解在controller参数前加Valid框架会在进入业务逻辑之前就把非法参数拦下来。这套组合的收益很大登录名非空、手机号格式、材料名称长度这些规则都声明在实体上不用在service里写一堆if判断。4. 认证授权与事项状态流转的实现细节4.1 JWT认证方案的实现细节身份认证方案我最终选了JWT没有用传统的Session。这和个人喜好无关主要是前后端分离架构下后端的服务是无状态的前端拿到token后每次请求在请求头中携带后端通过签名校验身份不需要在服务器内存里保存会话状态。登录流程是这样的用户提交账号密码后端校验通过后用登录用户的ID和角色信息生成一个带过期时间的token返回给前端前端把它放到LocalStorage中并在后续请求中通过Authorization请求头携带。token的密钥和过期时间建议放到配置文件中不要硬编码在Java类里。我一般会配置两个过期时间一个给普通访问token大约两小时一个给刷新用的refresh token可以设置到七天。刷新token可以被保存在安全策略更宽松的场景下前端检测到访问token过期时自动用refresh token换新的这样用户不需要频繁重新登录体验会舒服很多。当然这在毕业设计里属于加分项有精力可以加没有的话做好过期跳转即可。4.2 基于拦截器和注解的角色权限控制JWT解决了你是谁的问题接下来要解决你能做什么。权限控制我用了拦截器加自定义注解的方式没有引入Spring Security因为完全理解并配置好Spring Security的过滤器链本身需要不小的时间成本而这个项目的权限模型相对清晰用轻量方案反而更可控。登录拦截器处理所有带/api的请求校验token是否有效解析出的用户ID和角色存入ThreadLocal方便service层获取当前操作人。角色控制通过自定义RequireRole注解实现标注在controller方法上例如窗口人员提交预审接口要求角色是窗口人员后台用户管理接口要求角色是管理员。拦截器在进入方法前检查当前用户角色集合是否匹配注解要求不匹配直接返回无权限的响应。这样业务方法里不需要写权限判断逻辑干净权限调整也只需要改注解值。4.3 办件事项的状态机设计办件的状态流转如果只是简单地允许随意更新状态字段那么一段时间之后业务数据就全乱了。我把办件的生命周期抽象成状态机状态迁移必须满足合法路径。完整的状态集合是这样的草稿、已提交、预审通过、预审驳回、已受理、办理中、待补充材料、已办结、已评价。这里重点说一下待补充材料和预审驳回的区别。预审驳回意味着整个申请被退回申请流程终止用户可以修改材料后重新提交一个新申请而待补充材料是申请受理前窗口人员发现材料不齐全要求用户补充上传补齐后继续走后续流程而不是重新开始。这两个状态如果设计混淆后续统计报表里的申请量和退件量就会对不上。状态机用Java枚举实现枚举里定义每个状态可以合法迁移到的目标状态集合状态流转接口在执行业务前先检查当前状态是否允许迁移到目标状态。4.4 状态流转中的并发和幂等控制状态流转还有一个容易踩的坑并发和重复提交。用户在前端页面双击提交按钮可能会产生两个几乎同时到达后端的请求如果后端不做防护同一个申请单就有可能被提交两次生成两条办件记录。解决方式是在前端做提交按钮的loading和禁用同时在后端做幂等控制用办件编号和业务类型作为唯一约束重复提交时直接拦截。另一个并发场景是窗口人员和用户同时操作同一个办件窗口人员正在预审用户在另一端提交了补充材料这时候状态已经变了预审动作却还是基于旧状态执行的。这类问题没有特别精巧的解法最可靠的办法是在更新语句里加上状态条件比如UPDATE申请单SET状态新状态WHERE id某个ID AND 当前状态预期状态如果影响行数为零说明状态已被其他操作改变业务立即抛出冲突提示。5. 前端实现与前后端联调要点5.1 Vue工程初始化、路由与状态管理前端方面我创建了一个独立的Vue工程与后端完全分离开发。工程初始化时我用Vue CLI的交互式命令创建项目选择了Router、Vuex和Axios支持。这里提醒一个环境上的细节创建项目之前先把Node.js版本确认好Vue CLI 4或5对Node版本有要求版本太新或太旧都可能安装依赖时直接失败平时调试可以装好Vue DevTools插件组件状态和路由变化一眼就能看穿排查问题效率会高很多。路由设计上划分为两大区域不需要登录就能访问的页面例如办事指南列表、事项详情页需要登录才能访问的页面例如在线申请、进度查询以及只有特定角色才能访问的后台页面。这些通过路由守卫统一控制在beforeEach钩子里检查是否存在登录token无token则跳转到登录页并带上重定向地址登录成功后再跳回原目标页面。状态管理则用Vuex保存登录用户的基本信息和菜单权限刷新页面时从后端接口再拉一次保证刷新后权限不丢失。5.2 Axios封装与Token注入的细节前端网络请求的封装看起来是体力活但细节非常多。我在utils目录下封装了request.js导出一个自定义的axios实例并设置了统一的baseURL和超时时间。请求拦截器里做token注入从localStorage取出token后放入请求头Authorization字段没有token的请求照常发起响应拦截器里统一处理返回结果如果code是200直接将data字段返回给调用方业务方法拿到的就是干净的数据对象如果code是401则清除本地登录信息并跳转登录页其他业务错误码直接弹出message。这里有个细节经常被忽略文件上传接口的Content-Type是multipart/form-datatoken注入逻辑虽然一致但超时时间要单独调大否则大一点的附件在上传过程中很可能被axios默认超时杀掉。我一般会把上传接口的超时设到30秒以上。5.3 核心页面的组件化实现方式页面实现上最有代表性的两块是动态申请表单和进度查询步骤条。事项表里配置了每个事项需要的材料清单前端在线申请页面根据事项详情中的材料配置动态渲染上传区域和填写项。比如社保参保证明材料需要上传图片附件而单位证明还需要填写文件编号这些都是从配置字段读取后动态生成的。这样新增事项或者调整材料要求只需要后台管理配置好前端不用改任何代码。进度查询页面则把办件的状态轨迹可视化我封装了一个Steps展示组件后端提供当前状态和流转记录列表前端根据状态集合渲染步骤条。这里要注意后端应该返回流转记录的时间都用标准格式化字符串输出前后端约定好格式比如yyyy-MM-dd HH:mm:ss避免不同浏览器和时间时区导致的显示差异。5.4 联调阶段最常见的几个坑前后端联调是所有项目最花时间的阶段我把最常见的几个问题提前列出来省得大家走弯路。第一个是跨域问题。开发阶段前端跑在8080端口附近后端跑在8080端口浏览器会因为跨域拦截请求。我的做法是在前端开发服务器配置代理将/api路径代理到后端地址这样对浏览器来说所有请求都是同源的。这种做法比在后端配置CorsFilter更贴近生产环境因为生产环境本身也是通过Nginx反向代理将/api转发到后端。第二个是Long类型精度丢失。如果主键采用的是雪花算法生成的ID它超过了JavaScript安全整数的上限后端JSON序列化后前端拿到的ID末尾几位会变成0导致根据ID查询数据时查不到。解决方法是给主键字段加上JSON序列化注解将其转成字符串输出。这个问题非常隐蔽没有遇到过的同学很容易排查半天。第三个是日期格式统一问题。后端返回LocalDateTime字段默认序列化格式可能带有T字母前端直接展示会很难看我采用全局ObjectMapper配置让日期输出统一为指定格式同时前端展示时也按这个格式解析保证两边一致。6. 事务、缓存与查询性能的落地实践6.1 Transactional的正确使用方式在线申请提交这个核心操作涉及三个写入动作写入申请单主记录、写入申请材料记录、写入状态流转记录并更新办件状态。这三个操作必须在一个事务里执行任何一个失败都要回滚前序写入否则会出现申请单存在但材料记录缺失的脏数据。实现时在service方法上加Transactional注解即可。使用这个注解有两个经验要分享。第一是默认只回滚RuntimeException如果业务代码里抛出自定义的检查异常需要在注解里配置rollbackForException.class否则事务会静默提交不完整的数据。第二是事务方法必须从外部调用才生效如果同一个类里的方法直接调用Spring的AOP代理不会介入事务完全不生效很多人排查到最后才发现是这个问题。另外不要在一个大事务里去做远程调用或者网络请求长时间持有数据库连接在高并发下容易拖垮连接池。6.2 MyBatis缓存的边界与使用建议MyBatis自带一级缓存和二级缓存但实际使用时的边界要想清楚。一级缓存是SqlSession级别的同一个会话内同样的查询不重新查库默认开启在大多数场景下体验很好但要注意在分布式环境或长时间会话中可能读到陈旧数据。二级缓存是namespace级别的默认关闭开启后同一个Mapper下的查询结果可以被多个会话共享。我的建议是政务系统中不要轻易开二级缓存因为业务表经常被多个Mapper关联操作一旦某个Mapper更新了数据其他关联Mapper的缓存并不知道就会出现很隐蔽的数据不一致问题。与其靠缓存加速不如把热点数据放到Redis里由业务代码显式管理缓存失效。6.3 分页插件、索引与慢查询排查页面里的列表几乎全部需要分页我用的是PageHelper分页插件。使用上有严格的要求PageHelper.startPage方法必须写在查询语句之前且紧接着执行查询中间不能插入其他操作否则分页参数会被其他查询消费。我的习惯是分页代码统一写在service层controller只接收页码和页大小参数。数据库层面查询性能的瓶颈通常不是分页本身而是索引缺失导致的全表扫描。我的经验是给高频查询字段建立索引申请单表按当前状态过滤、按办件编号精确查询、按申请用户ID查个人办件、按创建时间做范围筛选都要有对应索引。这些都是标准的B树索引能解决的问题。上线之后如果觉得接口变慢我会先看后端日志里打印的SQL再用EXPLAIN命令查看执行计划重点看有没有出现Using filesort和临时表有的话基本就是索引或排序字段设计的问题。7. 从开发到上线的部署避坑清单7.1 环境与版本匹配是第一道坎部署环节最容易出问题的地方反而不是代码而是环境版本。SpringBoot当前版本很多如果选择了3.x那意味着JDK必须是17以上同时原来的javax命名空间全部换成了jakarta很多旧版依赖库会直接不兼容。我的建议是不要选太高版本稳定能用比追求新特性重要得多。SpringBoot 2.7附近配合JDK 8或11是目前大多数Java项目比较稳妥的组合。数据库这边MySQL 8和MySQL 5.7在驱动类名、默认认证方式上有差异连接字符串里的参数也要一起对应调整。我见过不少人在本地MySQL连不上报SSL连接错误就是因为连接串没有显式关闭SSL或者没有指定时区。只要把serverTimezoneAsia/Shanghai和useSSLfalse配好绝大多数连接问题都能解决。本地管理数据库用Navicat或开源替代工具都可以不要使用来路不明的破解版本安全问题不值得。7.2 前后端打包与发布流程打包发布我采用前后端分离部署的方式。后端执行Maven打包命令将项目打成可执行jar包可以加参数跳过单元测试以节省时间。打包完成后通过java -jar命令启动端口、数据库地址都从生产环境的配置文件中读取我习惯把生产配置单独放在application-prod.yml中用启动参数指定生效的配置这样不用每次发布都改代码。前端执行构建命令生成静态资源目录我通常用Nginx托管静态文件并把/api路径的请求反向代理到后端服务地址。这样做的好处是Nginx的并发处理能力比直接访问后端更稳静态资源和API分离也方便后续的扩展。需要注意前端Router如果用history模式Nginx需要配置try_files把路径都回退到index.html否则用户刷新页面就出现404。7.3 上线前后必须检查的高频问题上线前要检查的内容我习惯列成一个检查单过一遍。第一数据库账号是否只授权了应用需要的库不要用root账号跑生产。第二后端的日志路径是否配置日志级别是否合理没有日志的生产环境出了问题根本无法排查。第三前后端的跨域配置在生产环境是通过Nginx代理解决的要确认代理路径和接口路径完全一致。第四服务器防火墙和安全组是否只放行必要的端口比如80、443和数据库连接端口不对公网开放。上线之后仍然要盯的问题主要是接口响应时间和数据库连接数。响应突然变慢时先看慢SQL日志和后端GC情况数据库连接数被占满大多是因为连接池配置偏小或者有慢查询一直占用连接。这个问题在有定时任务批量扫描办件的时候尤其明显要注意定时任务的执行频率和每批处理的数据量。8. 源码结构导览与二次开发建议8.1 工程目录与模块说明后端工程的结构是标准的Maven单模块布局pom.xml维护依赖版本主代码按config、controller、service、mapper、entity、common、utils分包。config目录放拦截器注册、WebMvc配置和跨域配置common目录放Result统一响应、BizException自定义异常和GlobalExceptionHandler全局异常处理mapper目录里Java接口和XML文件分开存放XML放在resources下的mapper目录扫描路径在配置文件中指定。resources目录下的sql目录专门放初始化脚本。前端工程则按vue的标准结构组织api目录集中管理所有接口调用方法view目录按业务模块分子目录components目录放可复用的动态表单、步骤条、上传组件等router目录配置路由和守卫utils目录放axios实例和日期格式化工具。8.2 从零复现这个项目的操作路径如果你拿到这套源码准备跑通按下面的顺序操作最不容易出错。第一步创建数据库导入sql目录下的初始化脚本脚本里包含了建库建表语句和初始管理员账号的数据。第二步修改后端配置文件中的数据源地址、账号、密码必要时调整端口。第三步启动后端服务观察日志是否出现Tomcat started的提示。第四步进入前端目录安装依赖如果速度慢可以把npm镜像切换到国内源。第五步启动前端开发服务器浏览器访问本地端口用初始化账号登录。8.3 值得继续扩展的几个功能方向这套系统做完基础业务之后还有几个非常现实的功能方向可以继续深化。材料上传目前存的是本地磁盘路径生产环境比较合理的做法是接入MinIO或云对象存储把文件存储和业务数据解耦附件多了之后也方便做生命周期管理。事项检索目前是数据库模糊查询事项和办事指南的数据量变大以后可以引入全文检索或者HanLP分词方案搜索体验会明显提升。统计报表如果觉得前端表格不够直观可以用ECharts做图表展示配合定时统计每天各窗口的办件量管理端的大屏就能直接用了。办件数据如果需要导出给业务方做存档可以用Apache POI生成Word或Excel格式的文件而且POI本身也能生成图表办件趋势直接进文档问题不大。消息通知还可以进一步接入站内信和短信渠道在状态发生关键变化时主动触达用户。如果要再接一个面向群众的手机端入口后端这套REST接口基本可以直接复用前端用微信小程序或者App壳子重新写一套界面即可业务逻辑不需要大改。我在这个项目上最深的感受是技术栈本身并不复杂真正的复杂度都藏在业务边界和状态控制里。所有在线上环境看起来不起眼的细节比如承诺时限的计算、重复提交的拦截、状态迁移的合法性校验、部署时的版本匹配都是在开发阶段就必须提前考虑到的。项目做到这个程度已经可以实实在在跑起来支撑业务了剩下的优化就看实际使用中反馈什么需求再一步步往前迭代。