
之前在做后台系统功能清单梳理时我又看到了那个编号为 149 的需求单一个上线了三个多月、调用量几乎为零的“操作日志导出 PDF”功能。当初产品评审时它被描述成“管理员需要定期导出日志留档”开发也投入了 3 人天结果上线后真正用到的次数一只手数得过来。这种“没用的功能”在很多项目里都不少见但很少会有人正视它既没有人主动提下线也不敢轻易删最后它就像一块没有症状的病灶安静地躺在代码库里持续消耗着维护成本。这篇文章就围绕“没用的功能”这个话题展开从它到底是怎么产生的、如何用数据判断一个功能是否真的没用、到决定下线后如何安全地从代码中移除结合 Spring Boot 示例工程做一次完整实战。不管你是后端开发、前端开发还是经常参与需求评审的产品和技术负责人这篇文章的思路都能直接用。1. 背景与核心概念1.1 从编号 149 说起一个功能的诞生到沉寂为了便于讨论先假设这样一个典型场景。某个后台管理系统里运营团队在某一轮迭代中提出需求希望在操作日志模块增加“导出 PDF”功能理由是“领导需要定期导出操作记录用于归档”。这个需求经过评审后进入开发排期分配给后端工程师。后端用了一周时间实现导出逻辑、处理中文 PDF 字体、设计模板然后测试通过、上线发布。三个月后团队做功能使用率盘点发现这个接口的累计调用次数只有 23 次。进一步分析发现其中 20 次还是测试环境联调时打的生产环境实际只有 3 次调用。这就是一个非常典型的“没用的功能”它有明确的需求来源有完整的开发过程甚至都经过了测试但上线后并没有真实用户使用。它占据了代码量、测试用例、接口文档、权限配置等多个位置却没有产生对应的业务价值。1.2 没用的功能不代表“没有价值”在讨论这个话题前要先区分一个容易混淆的概念一个功能“没用”并不是说它一定没有价值。从不同视角看“没用”有三种含义用户视角的没用用户根本不点、不用在页面上完全感知不到它的存在。比如某个后台的一个复杂筛选组件90% 的用户只用默认条件。业务视角的没用功能本身被使用了但没有带来业务结果。比如让用户填写一份超长的调查问卷填的人不少但最终没有形成任何运营决策。技术视角的没用功能被调用了但完全可以被另一个成熟方案替代。比如自己实现了一套文件预览工具而项目里其实已经引入了 OSS 自带的预览能力。这三类“没用”需要不同的处理方式。用户不用可能需要优化入口或干脆下线业务不产生结果可能需要重新设计指标技术重复可能需要做能力收敛和替换。1.3 为什么“没用功能”是开发者的隐形负担一个没用的功能伤害往往不会立刻暴露。它更像是慢性消耗。第一是维护成本。只要功能还活着它就要参与编译、测试、回归。某个老功能使用了一个已经停止维护的第三方库每次升级依赖都要为它单独做兼容这就是最典型的隐性成本。第二是认知负担。新接手项目的开发者在阅读代码时会以为每个功能都有其存在的理由。如果一个导出 PDF 的功能其实根本没人在用新人可能会在它基础上继续开发“导出 Excel”导致代码越来越臃肿。第三是故障面。只要代码还在它就可能出问题。一个没人用的功能报错了凌晨触发报警值班人员爬起来排查最后发现这个接口根本没人调用这种情况在真实项目中非常常见。第四是测试成本。自动化用例里如果覆盖了这个功能每次 CI 都要为它支付时间成本如果不覆盖它又会成为一段不敢动的“神秘代码”。所以识别和清理没用的功能不是简单的“删代码”而是对项目和团队效率的一次投资。2. 无用功能是怎么一步步出现的想要清理没用的功能先得知道它们是怎么来的。从我的经验看无用功能通常是在四个环节里被“制造”出来的。2.1 需求评审阶段伪需求与需求蔓延很多没用的功能在需求阶段就有征兆。最常见的是“伪需求”提出方描述了一个场景但这个场景本身是否真实存在没有验证。比如“管理员需要导出 PDF”看起来没问题但实际工作中管理员可能更习惯直接在系统里看或者根本不需要存档。需求蔓延则更隐蔽。一个 CRM 项目原本只需要三个状态新建、跟进、关闭。评审时有人提出“加一个暂缓状态吧万一以后用得到”于是状态变成了四个后来变成八个。每个状态都要处理流转规则、权限、统计报表最后其中三个状态上线后一次都没被使用过。这里要意识到一个问题需求评审时如果只讨论“能不能做”不讨论“做了之后怎么验证有没有用”那大概率会做出没人用的功能。2.2 产品设计阶段照搬竞品与过度设计还有一种常见的来源是“对标竞品”。看到竞品做了某个功能自己也跟着做这是很多业务团队的惯性思维。但竞品做这个功能背后可能有完全不同的用户群体、使用场景和运营策略。照搬下来的功能往往和自己的用户习惯脱节。过度设计也容易制造没用功能。典型表现有为根本不存在的用户角色设计权限粒度为一个只有几十条数据的表设计分页、筛选、排序、导出全套能力在用户量还是个位数的时候就开始做消息队列、分布式事务和复杂的缓存策略。这些设计在纸面上很完整但和真实的业务发展阶段不匹配。2.3 技术实现阶段预埋能力与过度封装技术侧同样会制造“没用功能”。一种是“预埋能力”。开发者在实现某个需求时出于“以后可能用得上”的考虑顺手把参数做得非常通用支持多种类型、多个来源、多个回调地址。但后续业务根本没有这个需求这些参数就一直使用默认值。表面上这是“扩展性好”实际上这些参数会污染接口文档、增加调用方理解成本还可能被错误使用。另一种是重复造轮子。团队内已经存在一个统一的文件处理服务但某个项目为了“减少耦合”自己又写了一套上传下载逻辑。这种重复能力短期看不出问题但一旦基础组件升级两套逻辑都要改。2.4 组织协作层面缺少数据反馈闭环最后一个原因是团队没有建立“功能使用反馈”的机制。很多功能上线后团队关心的是“有没有 Bug”“响应时间是多少”却很少关心“用户到底有没有在用”。如果产品经理不去看埋点数据开发不看接口调用日志那么这个功能无论有没有人用都不会有人发现。时间一长产品经理换人了开发也换了新接手的人面对一个老功能不知道该不该删于是继续保留。无用功能就这样沉淀下来了。3. 如何识别一个功能“没用”识别没用功能不能靠感觉要靠证据。证据通常来自三类数据、代码、用户反馈。3.1 用数据说话核心指标怎么选判断一个功能是否有用最直接的就是看“使用频率”。不同功能需要定义不同指标。对于后台管理类功能可以看接口调用量、活跃用户数、调用成功率。对于 C 端页面功能可以看 PV/UV、点击率、停留时长、下一步转化率。对于工具类功能如导出、导入可以看执行次数、导出条数、用户分布。对于报表类功能可以看访问人数、使用时长、分享/下载次数。关键不是指标多而是要在功能上线前就把“有用”定义清楚。比如导出 PDF 功能如果上线三个月、月活管理员中只有不到 1% 使用那就基本可以判定为边缘功能。3.2 埋点是基础接口访问统计的常见方式判断功能是否有人用需要数据。数据从哪来常见有三种方式。第一种是前端埋点。在页面按钮点击、路由切换时上报事件可以精准知道用户点了哪里。适合判断页面级功能的使用情况。第二种是后端访问日志。通过中间件统一打印每个接口的访问情况或者由网关层统一统计。适合判断接口级功能是否被调用。第三种是数据库流水。如果某个功能每次操作都会写一条记录比如操作日志、调用记录可以直接查表统计。这种方式最准确但要求业务代码本身具备记录能力。实际项目中通常会把前后端数据结合使用。前端埋点能反映“用户点了”后端日志能反映“接口真的被调了”两者对比还能发现前端按钮暴露了但接口调用失败之类的问题。3.3 代码层面的识别信号除了数据代码本身也会暴露问题。一个功能没有对应的测试用例说明开发时就不受重视。一个功能的日志级别长期是 debug说明很少有人关注它的运行状态。一个功能在最近几次迭代中频繁被改动但改动原因都是“兼容性调整”而不是“新增业务需求”。一个功能对外提供的接口调用量日志里常年为零。一个功能的实现代码里大量使用if (xxx null) return;这类防御式判断说明它对异常场景很敏感但真实用户可能根本没触发过。另外还可以借助静态分析工具比如 Java 项目里的 ArchUnit 或 IDE 自带依赖分析找出那些“没有被其他模块引用”的类和方法。虽然不引用不代表没用但配合数据看会更有说服力。3.4 用户反馈与客服工单有些功能虽然没人主动用但在某些时刻会被“想起来”。用户反馈渠道就是捕捉这些信息的地方。如果客服系统里经常出现“这个功能在哪里”“点了没反应”“导出后打不开”之类的工单说明用户是有使用意愿的只是功能体验不好这种情况应该优化而不是下线。反过来如果用户根本不知道有这个功能或者在调研中说“没听说过”那就说明功能的存在感和使用场景都很弱进一步验证了“没用”的判断。4. 功能评估矩阵决定“下线”还是“保留”识别出一个功能疑似没用之后不要急着删除。我们需要一套评估机制来判断功能到底应该下线、保留还是优化。4.1 评估维度我在项目里常用下面六个维度。使用频率核心指标。可以看日活、月活、调用次数。区分高频、低频、极低频。业务价值这个功能是核心业务链路中的一环还是锦上添花哪怕用的人少如果是合规必需就绝不能下。维护成本代码复杂度、依赖数量、是否引入第三方服务、是否阻塞发版。替代方案是否存在替代功能或替代渠道。比如导出 PDF 没人用但导出 Excel 使用率很高那说明用户只是不喜欢 PDF而不是不需要导出。依赖关系该功能被哪些模块依赖反向依赖哪些模块。链路下游有强依赖时不能简单下线。合规与安全某些功能即使没人用也必须存在。比如审计日志、数据保留策略、风控校验。4.2 打分表模板可以把维度做成一张打分表每个维度 1 到 5 分。维度1 分3 分5 分使用频率近 30 天几乎无调用偶有低频调用高频使用业务价值非核心链路可用可不辅助业务但非必需核心链路必备维护成本复杂度高依赖多中等复杂度简单稳定替代方案有现成替代方案部分可替代无替代依赖关系无依赖或被依赖方少有少量依赖强依赖不可摘除合规与安全无合规要求有一定关联审计/合规强要求打分后综合判断如果“使用频率”低且“替代方案”充足同时“合规与安全”不是强要求那这个功能就具备下线条件。如果“合规与安全”高哪怕没人用也需要保留不能乱删。4.3 三种决策下线、保留、优化评估结果无非三种。下线确认没价值、没依赖、没合规要求可以进入下线流程。保留使用频率低但它是核心链路或合规要求的一部分保留但标注为“低频高价值功能”。优化功能本身有价值但入口太深、体验太差导致没人用。这种情况需要做导航调整、交互优化或入口引导而不是下线。回到编号 149 那个导出 PDF 功能按这个矩阵打分使用频率 1 分业务价值 2 分维护成本 3 分替代方案 4 分依赖关系 3 分合规与安全 2 分。综合下来属于可以下线的类型。5. 实战案例用 Spring Boot 实现一个“无用功能扫描器”为了把上面的方法论落地我们用一个 Spring Boot 示例工程来演示两个关键能力统计每个接口的调用次数输出低频接口列表。通过配置开关将某个疑似无用的功能快速下线而不影响其他模块。这里的示例以常见环境为例版本需要根据你的项目实际情况调整重点演示配置思路。5.1 环境与项目结构开发工具IntelliJ IDEA构建工具Maven框架Spring Boot 2.x / 3.xJDK8 或 11 及以上数据库不需要示例直接用内存统计项目结构如下src/main/java/com/example/uselessfeature ├── UselessFeatureApplication.java ├── config │ └── WebConfig.java ├── interceptor │ └── ApiUsageInterceptor.java ├── controller │ ├── OperationLogController.java │ └── FeatureStatusController.java └── task └── ApiUsageReportTask.java假设项目里已经有一个导出 PDF 的接口对应功能编号 149。5.2 实现接口调用统计第一步创建一个拦截器在每次请求进入 Controller 前对接口调用次数进行累加。为了简单这里使用内存 Map 保存数据。// 文件路径src/main/java/com/example/uselessfeature/interceptor/ApiUsageInterceptor.java package com.example.uselessfeature.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public class ApiUsageInterceptor implements HandlerInterceptor { private final MapString, AtomicLong usageMap new ConcurrentHashMap(); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod handlerMethod) { String key handlerMethod.getBeanType().getSimpleName() # handlerMethod.getMethod().getName(); usageMap.computeIfAbsent(key, k - new AtomicLong()).incrementAndGet(); } return true; } public MapString, Long getUsageSnapshot() { MapString, Long snapshot new ConcurrentHashMap(); usageMap.forEach((key, value) - snapshot.put(key, value.get())); return snapshot; } public void reset() { usageMap.clear(); } }说明这里使用HandlerMethod判断请求是否映射到了具体的方法排在非 Controller 请求。key 由类名#方法名组成避免 URL 中携带路径参数导致统计失准。使用ConcurrentHashMap和AtomicLong保证并发下数据正确。第二步通过配置类注册这个拦截器。// 文件路径src/main/java/com/example/uselessfeature/config/WebConfig.java package com.example.uselessfeature.config; import com.example.uselessfeature.interceptor.ApiUsageInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { private final ApiUsageInterceptor apiUsageInterceptor; public WebConfig(ApiUsageInterceptor apiUsageInterceptor) { this.apiUsageInterceptor apiUsageInterceptor; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(apiUsageInterceptor) .addPathPatterns(/**); } }注意这里需要把ApiUsageInterceptor注册为 Spring Bean否则无法注入。可以在类上加Component注解// 文件路径src/main/java/com/example/uselessfeature/interceptor/ApiUsageInterceptor.java Component public class ApiUsageInterceptor implements HandlerInterceptor { // 其余代码同上 }第三步创建一个定时任务每天输出一次接口调用统计。// 文件路径src/main/java/com/example/uselessfeature/task/ApiUsageReportTask.java package com.example.uselessfeature.task; import com.example.uselessfeature.interceptor.ApiUsageInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Map; Component public class ApiUsageReportTask { private static final Logger log LoggerFactory.getLogger(ApiUsageReportTask.class); private final ApiUsageInterceptor apiUsageInterceptor; public ApiUsageReportTask(ApiUsageInterceptor apiUsageInterceptor) { this.apiUsageInterceptor apiUsageInterceptor; } Scheduled(cron 0 0 2 * * ?) public void report() { MapString, Long snapshot apiUsageInterceptor.getUsageSnapshot(); log.info( API Usage Report ); snapshot.entrySet().stream() .sorted(Map.Entry.String, LongcomparingByValue().reversed()) .forEach(entry - log.info(API: {}, count: {}, entry.getKey(), entry.getValue())); log.info(); } }在主启动类上记得开启定时任务支持// 文件路径src/main/java/com/example/uselessfeature/UselessFeatureApplication.java package com.example.uselessfeature; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; EnableScheduling SpringBootApplication public class UselessFeatureApplication { public static void main(String[] args) { SpringApplication.run(UselessFeatureApplication.class, args); } }到这里一个最简单的接口调用统计工具就完成了。定时任务每天凌晨两点输出一次统计结果开发同学可以从中找到长期为 0 或极低的接口。5.3 实现低频接口报表日志毕竟不方便查看。更实用的做法是暴露一个查询接口让开发和测试能够随时查看当前 Top N 低频接口。// 文件路径src/main/java/com/example/uselessfeature/controller/FeatureStatusController.java package com.example.uselessfeature.controller; import com.example.uselessfeature.interceptor.ApiUsageInterceptor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; import java.util.stream.Collectors; RestController RequestMapping(/api/feature-status) public class FeatureStatusController { private final ApiUsageInterceptor apiUsageInterceptor; public FeatureStatusController(ApiUsageInterceptor apiUsageInterceptor) { this.apiUsageInterceptor apiUsageInterceptor; } GetMapping(/low-usage) public ListMap.EntryString, Long lowUsage(RequestParam(defaultValue 10) int topN) { return apiUsageInterceptor.getUsageSnapshot().entrySet().stream() .sorted(Map.Entry.String, LongcomparingByValue()) .limit(topN) .collect(Collectors.toList()); } }这时候可以请求curl http://localhost:8080/api/feature-status/low-usage?topN5预期输出类似[ { key: OperationLogController#exportPdf, value: 0 }, { key: OperationLogController#exportExcel, value: 3 } ]从这个结果就能直观看到exportPdf接口调用量为 0而exportExcel接口有 3 次调用。这说明用户存在“导出”需求但对 PDF 这个格式不感兴趣。5.4 通过配置开关安全下线功能确认某个功能没用之后最稳妥的做法不是立刻删除代码而是先“下线”再观察。这里推荐用配置开关控制。在application.yml中加入开关feature: export-pdf: enabled: false在 Controller 中判断开关状态// 文件路径src/main/java/com/example/uselessfeature/controller/OperationLogController.java package com.example.uselessfeature.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/api/operation-logs) public class OperationLogController { Value(${feature.export-pdf.enabled:false}) private boolean exportPdfEnabled; GetMapping(/export-pdf) public ResponseEntityString exportPdf() { if (!exportPdfEnabled) { return ResponseEntity.status(404).body(功能已下线); } // 这里写真正的 PDF 导出逻辑 return ResponseEntity.ok(export pdf success); } GetMapping(/export-excel) public ResponseEntityString exportExcel() { // 保留正常功能 return ResponseEntity.ok(export excel success); } }这样做的好处是不需要重新发布代码就能切回功能只需要修改配置后重启或借助配置中心动态刷新。如果开关关闭后用户反馈强烈可以立刻恢复。如果开关关闭后持续两周没有任何人反馈再考虑删除代码。如果想做得更彻底也可以使用 Spring 的条件注解在下线时直接不创建对应的 Bean。比如把 PDF 导出服务拆成一个独立类// 文件路径src/main/java/com/example/uselessfeature/service/OperationLogPdfExportService.java package com.example.uselessfeature.service; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; Service ConditionalOnProperty(name feature.export-pdf.enabled, havingValue true, matchIfMissing false) public class OperationLogPdfExportService { public String export() { return export pdf success; } }当配置为false时Spring 容器根本不会创建这个 Bean比在方法里判断开关更干净。5.5 运行与验证启动项目访问curl http://localhost:8080/api/operation-logs/export-pdf当feature.export-pdf.enabledfalse时返回 404功能已下线把配置改为true再重启重新访问返回export pdf success通过这个流程一个功能在被删除前会经历一个“可恢复下线”的观察期。这个观察期很有价值它能帮助团队确认“真的没有人在用”。6. 功能下线的最稳操作流程有了识别工具和开关机制下面整理一套标准的功能下线流程。不管是小功能还是大模块都建议按这个顺序执行。6.1 下线前检查清单在真正下线前至少确认下面这些问题功能近 30 天是否有真实调用统计口径要排除测试环境、自动化脚本和内部联调。是否有前端入口引导用户访问这个功能如果有前端需要同步移除入口。是否被其他接口或服务调用尤其是内部 API 调用不能只看前端。数据库表、配置项、缓存 key 是否被该功能独占如果是需要考虑数据迁移和清理。是否有合规或审计要求存在争议时先和法务/合规确认。是否有灰度发布条件开关方案是否已经就绪。如果下线后出现问题回滚方案是什么这七项确认完再动代码。6.2 灰度下线先开关再降级最后删除推荐的步骤是通过配置中心把功能开关切到关闭状态观察线上日志和用户反馈。观察期建议持续 1 到 2 个业务周期。如果功能是月报相关建议观察一整月。在观察期内不要急着删除代码也不要急着删数据库字段。确认无人反馈后再移除前端入口删除后端接口逻辑。最后清理无用依赖、无用配置项和相关测试用例。每一步都要有小版本发布不要在一个版本里同时做所有事。6.3 回滚方案无论流程多严谨都可能有意外。比如你认定没人用的功能某个客户刚好每个季度用一次这一季度正好撞上你下线的窗口。所以回滚方案必须提前定如果只是开关关闭那么重新打开开关即可最快。如果已经删除了代码就需要从 Git 历史中恢复并把上一个版本重新发布。如果删除了数据库字段回滚会麻烦得多。因此建议数据库变更延后到代码删除后的下一个版本。总之数据库变更永远比代码变更慢一步这是功能下线的重要原则。6.4 常见问题与排查问题现象常见原因解决思路开关配置不生效配置中心未刷新或使用了错误 key检查配置中心和本地配置优先级先重启验证功能下线后其他报表报错该功能被其他报表统计逻辑引用下线前用代码搜索确认所有引用点统计数据显示有调用但实际没人用定时任务、监控探针、内部调用产生访问排除非用户来源按用户维度或会话维度过滤接口已删除但前端入口还在前端和后端发布不同步先下线前端入口再删除后端接口删除代码后需要恢复Git 版本管理不清晰用独立 commit 提交删除方便 revert7. 工程建议让“没用功能”从源头变少清理功能是事后补救。更好的做法是从源头上减少无用功能的产生。7.1 需求评审阶段添加“验证指标”每一次需求评审除了问“做什么”和“怎么做”还应该问“怎么判断这个功能成功了”。比如导出 PDF 功能成功指标可以是“上线一个月内有 10 个以上管理员使用”。如果连验证指标都定不出来说明需求方自己也没想清楚这个功能的价值这时候就要谨慎了。7.2 用 MVP 思维替代大而全很多功能第一次做的时候不需要把所有选项、所有权限、所有导出格式都做出来。先提供一个最小可用版本验证用户是否需要这个功能再决定是否补齐能力。这和开发里的“先跑通再优化”是一个道理只是把思路从“代码实现”迁移到了“产品功能”上。7.3 定期做功能审计把功能审计加入版本节奏。比如每两个迭代做一次每次审计时间控制在半天以内。审计时拿上个月的接口调用数据列出调用量最低的 20 个接口逐个过一遍。这一步不需要技术负责人参与普通开发看完数据后就能给出初步判断。有争议的再拉上产品和测试一起讨论。7.4 代码删除也是一种重构删除代码时不要只删表面的一层。建议连同以下内容一起处理接口 Controller 方法。Service 层业务逻辑。对应实体类字段或独立表。配置项和常量类中的相关定义。前端页面入口和路由。测试用例和测试数据。不再使用的第三方依赖。删除后跑一遍全量测试再人工回归关联模块。把删除动作做成一个小而清晰的 PR commit message 写清楚“移除功能编号 149操作日志导出 PDF”。7.5 文档同步与知识沉淀功能下线后更新相关文档很关键。至少包括接口文档中标记该接口已下线或移除。产品需求文档中标注状态为“已下线”及原因。团队 Wiki 中如果有功能清单同步移除。如果是因为“没有价值”而下线可以简单写一句复盘提醒后人不要再做类似需求。这一步看起来很琐碎但能避免半年后有人再次提出同样的需求。8. 最后整理一份“功能下线检查单”文章的最后把最实操的部分沉淀成一份检查单。以后遇到疑似没用的功能直接对照执行就行。[ ] 拉取近 30 天接口调用数据排除测试和内部调用。[ ] 确认功能无合规、审计、安全要求。[ ] 全局搜索代码确认无上下游依赖。[ ] 通过配置开关关闭功能并开启观察期。[ ] 观察期至少覆盖一个完整业务周期。[ ] 观察期间无用户反馈和异常日志。[ ] 删除前端入口、后端逻辑、依赖和配置。[ ] 数据库字段变更延后到代码删除后的下一版本。[ ] 更新接口文档、需求文档和团队 Wiki。[ ] 独立提交完善 commit message方便随时 revert。回到最开始的编号 149它现在已经不是一个“没用的功能”而是一个提醒我们保持克制的例子。每个功能在进入代码库之前都应该先回答三个问题用户真的需要它吗有数据支撑吗如果删掉它谁会受到影响希望这篇文章能帮你在下一次项目梳理中更果断也更安全地识别和下线那些没用的功能。