新闻详情

新闻详情

首页 / 资讯中心 / 详情

从蜘蛛侠重启看接口多版本共存策略

发布时间:2026/9/7 1:47:33来源:尧图网络
从蜘蛛侠重启看接口多版本共存策略
最近荷兰弟聊蜘蛛侠十年的话题热度很高从初代三部曲到 MCU 阶段再到后续《崭新之日》《奥德赛》这些新项目很多粉丝都在感慨“一个时代结束了另一个时代又开始了”。作为一个常年写接口、做项目重构的技术博主我听到这类消息时的第一反应反而不是剧情走向而是三代蜘蛛侠的更替本质上就是一次漫长的系统版本演进旧版本被保留、新版本被引入、老客户端需要继续兼容、新生态又希望放开手脚。本文打算借这个话题聊一聊软件系统在长期演进过程中最常用的“多版本共存”方案。为了让内容不空谈我会用一个电影信息 API 的完整示例演示如何从 v1 平滑升级到 v2如何保留旧接口如何设计数据表迁移以及生产环境中应该注意哪些细节。无论你是刚入门 Spring Boot还是正在维护一套多年历史的旧系统这篇文章都能给你一些可以直接落地的参考。1. 从“蜘蛛侠十年”聊到系统演进1.1 一条新闻引发的“工程师联想”漫威影业过去十年把“荷兰弟”版蜘蛛侠放进了整个 MCU 大叙事里观众已经习惯了“看蜘蛛侠先要看复仇者联盟”的观看方式。现在 MCU 阶段落幕蜘蛛侠又要开启属于自己的新项目这种“告别一个阶段、开启一个新阶段”的过程像极了我们日常工作中的系统升级。举例来说对用户来说旧系统还开着不能直接停机。对产品来说新功能必须上线不能一直拖着。对开发来说既要保证老用户不受影响又要让新版本可以独立演进。这样一来最简单也最稳妥的思路就是“新旧版本共存”v1 继续服务老客户端v2 面向新需求中间通过网关、路由或代码层策略完成分流。蜘蛛侠系列电影从索尼自己的版本切换到 MCU 版本也没法直接删除旧版本而是用“重新启动”的方式在同一 IP 下延续不同宇宙故事。接口版本化也是同一个道理。1.2 三次重启对应三种工程策略回头看索尼和漫威对蜘蛛侠的三次重启会发现它们分别对应了软件工程里非常典型的三种应对方式阶段电影策略工程类比托比版到加菲版推倒重来新演员、新导演、新世界观推翻旧系统重写追求彻底重构加菲版到荷兰弟版再次重启但引入 MCU 关联重写核心模块并接入公司统一登录等基础平台MCU 版到新阶段告别旧叙事保留核心角色继续扩展保留核心接口废弃不必要依赖面向未来做扩展第三种策略在真实项目中其实最常用。推倒重写成本高、风险大多数团队都不会在没有足够测试覆盖和流量治理能力时贸然选择。更合理的方式是保住稳定的核心模型把外部关联慢慢解耦新增能力通过新版本接口暴露老接口逐步降级、废弃、下线。这也是本文示例里要演示的核心思路。1.3 本文适合哪些读者这篇文章的主体是一套 Spring Boot H2 的小型实战项目适合以下几类读者刚接触 Spring Boot想理解接口版本化怎么写。维护老项目正在考虑如何升级旧接口。做系统重构想知道数据库字段如何做到“只增不改”。对业务演进和软件架构感兴趣想找一个具体可运行的例子。文章会给出完整的代码结构你可以直接复制到本地运行也可以只阅读设计思路用到自己项目里。2. 环境准备与项目搭建2.1 技术选型为了让示例尽量轻量本文选择以下技术栈JDK 17Spring Boot 3 要求 Java 17 及以上。Spring Boot 3.x本文示例以 3.2 版本为基础你本地使用其他 3.x 版本也可运行。Spring Web提供 REST 接口。Spring JDBC H2用 H2 内存数据库演示建表和数据初始化避免依赖外部数据库。Maven项目构建与管理依赖。版本需要根据你本机的环境做调整。如果你正在公司项目中使用 Spring Boot 2.x那么部分配置会有差异但 URL 版本化的思路完全通用。2.2 创建 Maven 项目推荐使用 IDEA 直接创建 Spring Initializr 项目也可以手动创建 Maven 工程。项目名可以取spider-verse-api包名建议使用com.example.spiderverse。最终项目结构如下spider-verse-api ├── pom.xml └── src └── main ├── java │ └── com/example/spiderverse │ ├── SpiderverseApplication.java │ ├── controller │ │ ├── MovieControllerV1.java │ │ └── MovieControllerV2.java │ ├── dto │ │ ├── MovieV1.java │ │ └── MovieV2.java │ ├── model │ │ └── Movie.java │ ├── repository │ │ └── MovieRepository.java │ └── service │ └── MovieService.java └── resources ├── application.yml ├── schema.sql └── data.sql先创建启动类。package com.example.spiderverse; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class SpiderverseApplication { public static void main(String[] args) { SpringApplication.run(SpiderverseApplication.class, args); } }2.3 添加 Maven 依赖在pom.xml中加入 Web、JDBC 和 H2 依赖。如果你已经通过 IDEA 选了依赖可以跳过手工添加步骤。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency /dependencies说明一下spring-boot-starter-jdbc会帮助我们自动配置JdbcTemplate和DataSource配合 H2 可以在启动时执行schema.sql和data.sql非常适合本地演示。2.4 配置文件与初始化数据在resources目录下创建application.ymlspring: datasource: url: jdbc:h2:mem:spiderdb;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: sql: init: mode: always server: port: 8080spring.sql.init.modealways表示每次启动都执行初始化脚本这样本地演示时数据一定是完整状态。生产环境不要这样用生产应该由专门的迁移工具管理表结构。创建schema.sql定义电影信息表CREATE TABLE IF NOT EXISTS movie ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, hero VARCHAR(50) NOT NULL, universe VARCHAR(50) NOT NULL, release_year INT NOT NULL, generational_tag VARCHAR(20) NOT NULL, status VARCHAR(20) NOT NULL );再创建data.sql插入几条演示数据。这里需要提前说明下面的数据只用于演示结构不是真实片单或真实票房重点看字段变化。INSERT INTO movie (title, hero, universe, release_year, generational_tag, status) VALUES (Spider-Man: The First Chapter, Tobey, Sony, 2002, GEN1, FINISHED), (Spider-Man: Amazing Reboot, Andrew, Sony, 2012, GEN2, FINISHED), (Spider-Man: MCU Debut, Tom, MCU, 2017, GEN3, FINISHED), (Spider-Man: New Chapter, Tom, MCU, 2021, GEN3, FINISHED), (Spider-Man: Brand New Day, Tom, NextUniverse, 2026, GEN4, PLANNED);这张表里既有旧阶段的数据也有新项目的占位数据。generational_tag表示第几代故事线status表示当前状态。3. 核心概念版本演进与多版本兼容3.1 什么是破坏性变更接口升级时最怕的是“破坏性变更”。所谓破坏性变更就是老客户端无法继续使用原来的请求和响应。举例来说原本 v1 接口返回{ id: 1, title: Spider-Man: MCU Debut, hero: Tom, releaseYear: 2017 }如果你在 v2 里直接把releaseYear改名为year或者把hero从字符串改成对象老客户端解析时就会报错。这就是破坏性变更。蜘蛛侠电影重启也是一样如果剧组把主角改成“超级英雄团队群像”观众会发现这不是原来的蜘蛛侠故事了。所以软件系统在演进时要尽量做到“增量升级”新增字段、新增接口、废弃旧字段但不要立刻删除。3.2 接口版本化的常见方式主流 REST API 版本化有三种做法方式示例优点缺点URL 路径版本/api/v1/movies、/api/v2/movies直观方便路由和日志排查URL 中暴露版本号越往后路径越臃肿Header 版本Accept: application/vnd.movie.v2json路径干净版本和媒体类型绑定排查问题不够直观客户端配置复杂Query 参数版本/api/movies?version2变更成本低容易漏传缓存失效不好控制三种方式没有绝对优劣中小项目最推荐 URL 路径版本因为运维排障时一眼就能看出客户端调用的是哪个版本。本文示例也采用这种方式。3.3 数据库结构的最小变更原则接口版本化的背后数据库也不能频繁“伤筋动骨”。在实际项目里如果 v2 需要新增“宇宙”字段不建议直接修改原表后删除旧字段。更稳妥的做法是新增字段时设置默认值保证老数据不为空。不直接删除字段先停止写入再等一段时间后清理。数据库字段类型尽量宽裕避免后期扩展时又要改类型。如果有枚举值变化例如新阶段增加了NextUniverse要确认老程序读取时不会因为未知枚举而崩溃。我们的示例表在设计时已经预留了universe、generational_tag、status等字段。在真实项目中v1 到 v2 的升级可能只是“增加几个字段”和“新增一个统计接口”而不是把整张表重建一遍。3.4 正确使用 Deprecated 标记Java 里有一个很常见的注解Deprecated它不会阻止代码运行只是告诉外部调用者“这个方法不推荐继续使用了未来可能会下线”。放在接口上也是同理。v1 接口虽然还在运行但是可以在 Controller 上加上Deprecated同时在接口文档中标注“deprecated”让新项目不再继续接入。这种方式很贴合电影宇宙的“落幕”逻辑老阶段不会再铺开新故事但老故事仍然保留在资料库里老观众仍然随时可以回看。4. 实战电影信息 API 的 v1 与 v2接下来进入完整实战。我们的目标很简单v1 接口继续返回比较简单的字段。v2 接口返回更多新字段并新增一个“世代统计”接口。两个接口同时在线互不影响。4.1 定义实体类与 DTO先定义数据库对应的实体类Movie。它包含了我们表里的全部字段是内部模型。package com.example.spiderverse.model; public class Movie { private Long id; private String title; private String hero; private String universe; private Integer releaseYear; private String generation; private String status; public Long getId() { return id; } public void setId(Long id) { this.id id; } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getHero() { return hero; } public void setHero(String hero) { this.hero hero; } public String getUniverse() { return universe; } public void setUniverse(String universe) { this.universe universe; } public Integer getReleaseYear() { return releaseYear; } public void setReleaseYear(Integer releaseYear) { this.releaseYear releaseYear; } public String getGeneration() { return generation; } public void setGeneration(String generation) { this.generation generation; } public String getStatus() { return status; } public void setStatus(String status) { this.status status; } }再定义两个 DTO 对象。MovieV1是旧接口的响应模型MovieV2是新接口的响应模型。package com.example.spiderverse.dto; public class MovieV1 { private Long id; private String title; private String hero; private Integer releaseYear; public MovieV1() { } public MovieV1(Long id, String title, String hero, Integer releaseYear) { this.id id; this.title title; this.hero hero; this.releaseYear releaseYear; } public Long getId() { return id; } public void setId(Long id) { this.id id; } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getHero() { return hero; } public void setHero(String hero) { this.hero hero; } public Integer getReleaseYear() { return releaseYear; } public void setReleaseYear(Integer releaseYear) { this.releaseYear releaseYear; } }package com.example.spiderverse.dto; public class MovieV2 { private Long id; private String title; private String hero; private String universe; private Integer releaseYear; private String generation; private String status; public MovieV2() { } public MovieV2(Long id, String title, String hero, String universe, Integer releaseYear, String generation, String status) { this.id id; this.title title; this.hero hero; this.universe universe; this.releaseYear releaseYear; this.generation generation; this.status status; } public Long getId() { return id; } public void setId(Long id) { this.id id; } public String getTitle() { return title; } public void setTitle(String title) { this.title title; } public String getHero() { return hero; } public void setHero(String hero) { this.hero hero; } public String getUniverse() { return universe; } public void setUniverse(String universe) { this.universe universe; } public Integer getReleaseYear() { return releaseYear; } public void setReleaseYear(Integer releaseYear) { this.releaseYear releaseYear; } public String getGeneration() { return generation; } public void setGeneration(String generation) { this.generation generation; } public String getStatus() { return status; } public void setStatus(String status) { this.status status; } }这里有一点值得强调旧接口MovieV1没有暴露universe和generation字段。即使数据库已经存在这些字段只要 DTO 不返回线上响应就不会变化。这就是“最小影响原则”。4.2 编写 Repository 数据访问层使用JdbcTemplate查询所有电影信息。代码比较简单重点是 RowMapper 的字段映射。package com.example.spiderverse.repository; import com.example.spiderverse.model.Movie; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; Repository public class MovieRepository { private final JdbcTemplate jdbcTemplate; public MovieRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public ListMovie findAll() { String sql SELECT id, title, hero, universe, release_year, generational_tag, status FROM movie ORDER BY release_year; return jdbcTemplate.query(sql, (rs, rowNum) - { Movie movie new Movie(); movie.setId(rs.getLong(id)); movie.setTitle(rs.getString(title)); movie.setHero(rs.getString(hero)); movie.setUniverse(rs.getString(universe)); movie.setReleaseYear(rs.getInt(release_year)); movie.setGeneration(rs.getString(generational_tag)); movie.setStatus(rs.getString(status)); return movie; }); } }如果你所在项目使用 MyBatis-Plus 或 Spring Data JPA查询方式会略有不同但 Repository 层的职责是一样的把数据库行映射成 Java 对象。4.3 编写 Service 业务层Service 负责把内部Movie对象转换成不同版本的 DTO。package com.example.spiderverse.service; import com.example.spiderverse.dto.MovieV1; import com.example.spiderverse.dto.MovieV2; import com.example.spiderverse.model.Movie; import com.example.spiderverse.repository.MovieRepository; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; Service public class MovieService { private final MovieRepository movieRepository; public MovieService(MovieRepository movieRepository) { this.movieRepository movieRepository; } public ListMovieV1 listV1() { return movieRepository.findAll().stream() .map(movie - new MovieV1( movie.getId(), movie.getTitle(), movie.getHero(), movie.getReleaseYear() )) .collect(Collectors.toList()); } public ListMovieV2 listV2() { return movieRepository.findAll().stream() .map(movie - new MovieV2( movie.getId(), movie.getTitle(), movie.getHero(), movie.getUniverse(), movie.getReleaseYear(), movie.getGeneration(), movie.getStatus() )) .collect(Collectors.toList()); } public ListMapString, Object generationStatistics() { MapString, Long grouped movieRepository.findAll().stream() .collect(Collectors.groupingBy( Movie::getGeneration, LinkedHashMap::new, Collectors.counting() )); return grouped.entrySet().stream() .map(entry - { MapString, Object item new HashMap(); item.put(generation, entry.getKey()); item.put(count, entry.getValue()); return item; }) .collect(Collectors.toList()); } }listV1和listV2的区别在于返回字段的多少。generationStatistics则按照generational_tag分组统计电影数量。演示数据里 GEN3 有两条其他各一条所以统计结果能直观看到“某个世代电影数量更多”。这里要多说一句真实项目中不要把所有版本转换逻辑都堆在 Service 里。如果版本很多建议拆出独立的 Assembler 或 Converter 类避免 Service 越写越臃肿。4.4 编写 v1 和 v2 两层 Controllerv1 Controller 是一个“老接口”为了提示客户端不要继续接入可以在类上增加Deprecated。package com.example.spiderverse.controller; import com.example.spiderverse.dto.MovieV1; import com.example.spiderverse.service.MovieService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; RestController RequestMapping(/api/v1/movies) Deprecated public class MovieControllerV1 { private final MovieService movieService; public MovieControllerV1(MovieService movieService) { this.movieService movieService; } GetMapping public ListMovieV1 list() { return movieService.listV1(); } }v2 Controller 则提供新的完整字段和统计接口。package com.example.spiderverse.controller; import com.example.spiderverse.dto.MovieV2; import com.example.spiderverse.service.MovieService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; RestController RequestMapping(/api/v2/movies) public class MovieControllerV2 { private final MovieService movieService; public MovieControllerV2(MovieService movieService) { this.movieService movieService; } GetMapping public ListMovieV2 list() { return movieService.listV2(); } GetMapping(/statistics/generations) public ListMapString, Object generationStatistics() { return movieService.generationStatistics(); } }到这一步一个最简单的“新旧版本共存”示例就完成了。v1 和 v2 虽然共用同一个数据表但对外暴露的能力不同旧客户端继续访问 v1 不会受影响新客户端直接使用 v2 获取更丰富的信息。4.5 运行与验证在项目根目录执行mvn spring-boot:run如果使用 IDEA也可以直接运行SpiderverseApplication的 main 方法。启动日志一般会显示Tomcat started on port(s): 8080 (http)然后打开新终端验证接口。先访问 v1 接口curl http://localhost:8080/api/v1/movies预期响应是只包含 id、title、hero、releaseYear 的 JSON 数组。由于 data.sql 中的数据顺序按 release_year 升序返回结果大致如下[ { id: 1, title: Spider-Man: The First Chapter, hero: Tobey, releaseYear: 2002 }, { id: 2, title: Spider-Man: Amazing Reboot, hero: Andrew, releaseYear: 2012 }, { id: 3, title: Spider-Man: MCU Debut, hero: Tom, releaseYear: 2017 } ]再访问 v2 接口curl http://localhost:8080/api/v2/movies这时候响应里会多出universe、generation、status字段[ { id: 1, title: Spider-Man: The First Chapter, hero: Tobey, universe: Sony, releaseYear: 2002, generation: GEN1, status: FINISHED }, { id: 2, title: Spider-Man: Amazing Reboot, hero: Andrew, universe: Sony, releaseYear: 2012, generation: GEN2, status: FINISHED }, { id: 3, title: Spider-Man: MCU Debut, hero: Tom, universe: MCU, releaseYear: 2017, generation: GEN3, status: FINISHED } ]最后访问统计接口curl http://localhost:8080/api/v2/movies/statistics/generations预期响应中能看出不同世代的电影数量[ { generation: GEN1, count: 1 }, { generation: GEN2, count: 1 }, { generation: GEN3, count: 2 }, { generation: GEN4, count: 1 } ]从结果可以看到v1 和 v2 共用同一份数据但返回的信息粒度不同。这个粒度差异不是临时代码而是版本策略的一部分。5. 常见问题与排查思路示例跑起来之后你可能还会遇到一些典型问题。下面这张表整理了最常见的几种现象和排查方向。问题现象常见原因解决思路启动时报Failed to determine a suitable driver class数据源地址或驱动类配置不正确检查application.yml中的spring.datasource.url和 driver 配置启动后提示找不到表MOVIEschema.sql未执行确认文件存在于src/main/resources并检查spring.sql.init.mode是否为always访问 v2 接口返回 404路由写错或 Controller 没有被扫描检查RestController、RequestMapping路径并确认 Controller 包在启动类子包下v1 和 v2 返回结果完全一样两个 DTO 字段设计重复没有体现差异重新设计 DTO让 v1 保持旧字段v2 增加新字段数据出现中文乱码数据库连接没有设置编码H2 内存库一般不会乱码如果是 MySQL需要在 JDBC URL 后增加characterEncodingutf8统计接口顺序不稳定分组默认使用 HashMap使用LinkedHashMap保持插入顺序或对结果统一排序如果启动时发现 v1 接口虽然能访问但控制台提示“已过时”这是Deprecated的正常表现。它只是警告不会阻断运行。很多时候接口升级报错并不在代码本身而在部署环境。比如网关层已经配置了/api/v1/*的白名单新增的/api/v2/*路径没有放行就会出现“本地能通线上 404”的情况。碰到这种问题第一反应不要改代码先抓请求日志确认请求到底进了哪一层。6. 工程实践建议6.1 尽早确定版本策略接口版本化最怕“随缘”。有的接口带路径版本有的接口带 Header 版本还有的接口只在参数里加一个version时间一长客户端自己都搞不清楚。建议一个团队只确定一种版本策略。如果历史包袱不重推荐使用 URL 路径版本访问日志直接能看到版本。Nginx、Gateway 可以针对不同路径做不同策略。不同版本的限流、降级规则容易配置。如果希望 URL 保持干净也可以统一用 Header 版本但要确保所有客户端 SDK 都支持自定义请求头。6.2 用自动化测试保护兼容性版本共存最怕“改坏老接口”。哪怕你只是给数据库加了一个字段也可能因为 RowMapper 顺序写错导致老接口报错。建议至少写一层 MockMvc 测试验证 v1 接口返回的字段结构与 v2 不同而且不会互相影响。下面给一个简短的测试思路。mockMvc.perform(get(/api/v1/movies)) .andExpect(status().isOk()) .andExpect(jsonPath($[0].hero).exists()) .andExpect(jsonPath($[0].universe).doesNotExist());这种测试不复杂却能在后续升级中提供很强的安全感。尤其是老接口准备下线前如果测试覆盖率不够不要轻易动手。6.3 数据库变更尽量向前兼容数据库字段一旦上线修改成本往往比接口还高。原因在于接口可以版本化但数据库是全局共享的。推荐几条执行原则新增字段设置默认值避免老程序查询时拿不到数据。不直接删除字段先停止代码写入再等确认后清理。修改字段类型要谨慎尤其是把字符串改长可以改短很容易导致线上报错。迁移前先备份迁移过程中用脚本统计影响行数。如果团队有 DBA建议所有 DDL 都走 review。如果没有 DBA也要让经验更丰富的同事参与评审。6.4 不要盲目“推倒重来”托比版到加菲版是一次完全重启但在真实项目里完全重写的成功概率往往不高。老系统里的业务规则、边界条件、历史 bug 修复逻辑都隐藏在代码和数据库里。推倒重写后很多东西会被无意间丢掉。更稳妥的路径是先梳理清楚旧系统的核心能力。确定哪些能力要保留、哪些要废弃。新系统先做“翻译层”继续对接旧客户端。逐步迁移流量直到旧系统可以下线。这个过程就像《崭新之日》开启的新阶段核心角色不变但叙事方式、世界观、技术细节都重新优化。6.5 配置隔离、日志与回滚如果你准备在生产环境上线 v2 接口建议把测试环境和生产环境的配置分开。数据库连接、日志级别、第三方接口地址都不能混在一起。上线前要确认v2 接口的错误日志是否单独可以检索。网关是否给 v2 路径配置了独立限流。如果 v2 接口发生兼容问题能否快速切回 v1。数据库脚本是否有回滚方案。日志方面推荐在每个版本的 Controller 入口打印清晰的请求日志。例如[api-v2] request /api/v2/movies from client demo-app这样排查问题时可以快速判断到底是配置问题、路由问题还是数据问题。7. 总结这篇文章从“荷兰弟聊蜘蛛侠十年”这个话题切入重点分析了软件系统在长期演进中常用的“多版本共存”策略。我们用一个 Spring Boot H2 的小示例实现了 v1 和 v2 两套接口同时对外提供服务v1 保持旧字段不变v2 返回更丰富的信息并额外提供了世代统计接口。如果你正在维护一套老系统下次遇到“新阶段来了旧接口怎么办”这个问题时可以试试这个思路先确认核心数据不重构再设计新接口保留旧接口并标记 Deprecated最后通过流量切换把老客户端平滑迁移过来。版本升级不需要一夜之间完成像蜘蛛侠十年接力一样每一步都稳一点后面的路反而会轻松一些。如果你对接口版本化、数据库平滑迁移还有什么问题欢迎在评论区交流。也可以把这篇收藏下来等项目排期需要做新老接口兼容时直接翻出来当参考。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

计算机仿真设计报告全攻略:选题建模到PDF排版避坑指南 2026/9/7 2:26:39

计算机仿真设计报告全攻略:选题建模到PDF排版避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
钢铁表面缺陷检测数据集实战:1800张含标签图像与模型训练指南 2026/9/7 2:26:39

钢铁表面缺陷检测数据集实战:1800张含标签图像与模型训练指南

简介:钢铁表面缺陷检测图像数据集面向工业视觉、机器学习与计算机视觉领域的算法工程师与研究者,专为钢铁产线表面缺陷自动识别与分类任务而构建,覆盖裂纹、锈蚀、凹坑、划痕等典型缺陷,可用于监督学习下的模型训练、调参与性能评…

阅读更多 →
不会电脑也能轻松上手:云端进销存选型与使用指南 2026/9/7 2:26:39

不会电脑也能轻松上手:云端进销存选型与使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
相控阵雷达原理与工程实践:从相位差到有源阵列 2026/9/7 2:26:39

相控阵雷达原理与工程实践:从相位差到有源阵列

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于SpringBoot和Vue的残疾人精准帮扶平台:技术栈、背景意义与核心代码 2026/9/7 2:26:39

基于SpringBoot和Vue的残疾人精准帮扶平台:技术栈、背景意义与核心代码

1. 项目背景与意义残疾人群体是社会的重要组成部分,其生活保障与就业帮扶一直是民生工作的重点。传统的帮扶工作多依赖线下走访、人工登记和纸质档案,存在信息分散、数据更新滞后、帮扶资源分配不均等问题。基层工作人员难以全面掌握辖区内残疾人的实际需…

阅读更多 →
WTL实战指南:用C++模板库打造轻量级原生Windows桌面工具 2026/9/7 2:23:38

WTL实战指南:用C++模板库打造轻量级原生Windows桌面工具

简介:WTL教程合集是一套面向Windows C开发者的系统学习资料,聚焦WTL这一轻量级MFC替代方案,帮助开发者利用模板类高效构建更小、更快、更可控的桌面程序。内容包括环境搭建与入门示例、窗口和控件封装、消息映射与事件处理、对话框/菜单/工具…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞