
摘要在老系统中做需求最怕的不是写代码而是不清楚应该改哪里、复用哪里、绕开哪里。本文结合一个续期管理后台中的“规则中心配置页”需求聊聊在 Spring Boot MyBatis 多模块项目里如何用较小改动完成一次稳定的增量开发。1. 背景很多企业项目不是从零开始的而是在已有系统上不断迭代。常见结构大概是project-root├── project-api├── project-common├── project-core├── project-provider├── project-server└── project-web这类项目通常有几个特点Controller 层在 web 模块业务 Service 在 core 模块DTO、Entity、Param 在 api 模块通用枚举、工具类在 common 模块外部系统调用或消息处理在 provider/server 模块MyBatis Mapper 和 XML 通常集中在 core。如果新需求一上来就大改底层逻辑风险会非常高。更稳妥的方式是先定位现有链路再基于现有架构做增量封装。2. 增量需求的拆解方式以“新增一个配置页”为例页面可能包含配置列表查询新增配置删除配置批量提交操作历史异常清单下载权限控制。后端可以拆成以下几层Controller↓Application Service↓Domain / Legacy Service↓Mapper / External Provider关键点是新 Service 不一定要重写业务逻辑而是作为“编排层”存在。例如publicinterfaceDataConfigService{ListProductOptionlistProductOptions(Stringkeyword);voidaddConfig(IntegerproductId,StringproductName,LongoperatorId);voiddeleteConfig(IntegerproductId,LongoperatorId);BatchResultbatchRepair(Stringids,StringoperatorNo,StringoperatorName);PageResultImportRecordDTOpageRecords(RecordQueryquery);}这个接口并不关心底层数据到底来自老表、新表、外部接口还是消息处理链路。它的职责是把页面需要的能力整理成稳定接口。3. Controller 层只做三件事在老项目里Controller 很容易越写越厚。我的经验是尽量让 Controller 只做三件事接收参数获取当前操作人调用 Service 并返回统一结果。示例PostMapping(/config/add)publicResultVoidaddConfig(RequestBodyConfigRequestrequest,HttpServletRequestservletRequest){EmployeeemployeegetCurrentEmployee(servletRequest);dataConfigService.addConfig(request.getProductId(),request.getProductName(),Long.valueOf(employee.getId()));returnResult.success();}这样做的好处是清晰参数校验在 Service 中统一处理操作人信息不散落在各处Controller 不感知底层业务细节后续单测更容易写。4. 复用旧逻辑而不是复制旧逻辑老系统里常常已经有一些能力比如根据投保单号查询主数据更新保单状态调用外部系统补齐数据写操作日志发送消息通知查询配置。新增功能时优先考虑“调用已有 Service”而不是把旧代码复制一份过来。错误示例// 把旧逻辑复制到新 Service 中后续两边都要维护privatevoidrepairData(Longid){// 一大段复制来的老逻辑}更好的方式privatevoidrepairData(Longid){legacyRepairService.repairById(id);}如果旧逻辑粒度太粗可以在外层做适配privatevoidrepairOne(Longid){try{legacyService.repair(id);}catch(Exceptione){thrownewBusinessException(normalizeMessage(e.getMessage()));}}这样新增功能就变成“业务编排”不是“底层重写”。5. 批处理要有记录和失败明细批量提交类需求不要只返回一句“处理完成”。更实用的模型是导入记录表├── 操作类型├── 操作人├── 总数├── 成功数├── 失败数├── 状态└── 操作时间失败明细表├── 记录ID├── 业务单号└── 失败原因批处理伪代码privateBatchResultexecuteBatch(ListLongids,Processorprocessor){ImportRecordrecordcreateRecord(ids.size());intsuccess0;ListImportErrorerrorsnewArrayList();for(Longid:ids){try{processor.process(id);success;}catch(Exceptione){errors.add(buildError(record.getId(),id,e.getMessage()));}}saveErrors(errors);updateRecord(record,success,errors.size());returnbuildResult(record);}这个设计有两个明显优势用户知道每次操作的结果失败数据可以下载后继续排查。6. 小结在多模块老系统中做增量需求重点不是“写更多代码”而是“把新功能安放在合适的位置”。建议遵循几个原则Controller 保持薄新增 Service 做编排复用旧业务链路Mapper 只承载必要查询批处理必须有历史记录权限点跟页面和按钮保持一致不做无关重构。老系统改造最重要的是稳。能小步扩展就不要大刀阔斧。