
1. 从一次真实踩坑说起MongoTemplate 分页为什么总差一条数据先说结论SpringBoot 整合 MongoDB用 MongoTemplate 做 CRUD 和分页接口最容易翻车的不是增删改查本身而是分页的skip/limit顺序、Criteria导错包、以及count和find用了两个不同的 Query 对象。这几个点我在实际项目里都踩过接口返回的total和data对不上前端翻到最后一页直接空白。这篇内容面向的是需要快速搭起 MongoDB 数据访问层的后端开发者。核心检索词就是 SpringBoot、MongoDB、MongoTemplate、CRUD、分页接口。我会给出一套可以直接复制的配置类、实体、Controller 代码把增删改查和分页查询跑通然后演示怎么把 AI 辅助编码工具的 endpoint 统一改到 TaoToken 管理 Key最后用接口调用和分页结果验证配置真的生效。MongoDB 是文档型数据库数据以 BSON 文档存储字段结构灵活适合字段经常变、嵌套结构多的场景。SpringBoot 通过 spring-boot-starter-data-mongodb 提供两种访问方式一种是 MongoRepository声明式接口简单但不够灵活另一种就是 MongoTemplate命令式操作能精确控制查询条件、排序、分页、投影做复杂后台接口时更顺手。为什么不用 MongoRepository 做分页因为它对动态条件的支持比较别扭多个可选查询条件拼起来要么写一堆方法名要么用 ExampleMatcher遇到范围查询、嵌套字段就力不从心。MongoTemplate 的QueryCriteria组合可以按需 addCriteria条件为空就跳过这正是后台列表接口最常见的需求。环境准备上你需要 JDK 8 或以上、Maven、一个可连接的 MongoDB 实例本地或远程都行以及 SpringBoot 2.x/3.x。下面所有代码在 SpringBoot 2.7 MongoDB 5.x 上实测通过。如果你本地没装 MongoDB用 Docker 起一个最省事docker run -d --name mongo-dev -p 27017:27017 mongo:5.0启动后连接串就是mongodb://127.0.0.1:27017/test。注意数据库名test要写在 URI 里否则默认连到test库容易和你的预期不一致。接下来我会按「依赖与配置 → 实体与配置类 → CRUD 接口 → 分页接口 → TaoToken 统一 Key → 验证与排障」的顺序展开每一步都给可复制的代码。你可以边看边敲也可以直接抄进项目改包名。2. 依赖、yml 与 MongoTemplate 配置类把连接和序列化一次配好先看 pom.xml 需要哪些依赖。核心是 spring-boot-starter-data-mongodbWeb 和 Lombok 按需加。如果你要用 Swagger 调试接口再补 springfox 或 springdoc这里我用 springdoc-openapi兼容性更好。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.6.14/version /dependency /dependenciesapplication.yml 里配置连接信息。这里有个细节auto-index-creation建议设为 true让Indexed注解生效否则你标了索引也不会自动建。server: port: 8080 servlet: context-path: / spring: data: mongodb: uri: mongodb://127.0.0.1:27017/test auto-index-creation: true如果你的 MongoDB 开了认证URI 写成mongodb://user:passhost:port/db?authSourceadmin。生产环境别把密码硬编码在 yml 里用环境变量或配置中心注入。接下来是实体类。用Document指定集合名Id标主键Field覆盖默认字段名。MongoDB 默认把 Java 字段名原样存成文档 key如果你想要下划线风格或者和已有数据对齐就用Field显式指定。package com.example.mongo.entity; import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import java.time.LocalDateTime; Data Document(collection t_user) public class User { Id private String id; Indexed Field(userName) private String userName; Field(password) private String password; Field(age) private Integer age; Field(createTime) private LocalDateTime createTime; }注意Id对应的字段类型是 StringMongoDB 会自动生成 ObjectId 并转成字符串。如果你用 ObjectId 类型也可以但返回给前端时序列化格式不同String 更省心。配置类这块其实 SpringBoot 自动配置已经帮你建好了 MongoTemplate只要 URI 配对了就能直接Autowired注入。但如果你要自定义转换器比如 LocalDateTime 的序列化格式就需要手动扩展。下面这个配置类把日期格式统一成yyyy-MM-dd HH:mm:ss避免返回时间戳或 ISO 格式导致前端解析麻烦。package com.example.mongo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.List; Configuration public class MongoConfig { private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); Bean public MongoCustomConversions mongoCustomConversions() { ListConverter?, ? converters new ArrayList(); converters.add(new LocalDateTimeToDateConverter()); converters.add(new DateToLocalDateTimeConverter()); return new MongoCustomConversions(converters); } static class LocalDateTimeToDateConverter implements ConverterLocalDateTime, Date { Override public Date convert(LocalDateTime source) { return Date.from(source.atZone(ZoneId.systemDefault()).toInstant()); } } static class DateToLocalDateTimeConverter implements ConverterDate, LocalDateTime { Override public LocalDateTime convert(Date source) { return LocalDateTime.ofInstant(source.toInstant(), ZoneId.systemDefault()); } } }这里有个坑MongoCustomConversions 一旦自定义会覆盖默认的转换器集合。如果你只加了日期转换其他类型比如 BigDecimal可能受影响。稳妥做法是把默认转换器也加进来或者干脆不自定义用JsonFormat在实体字段上控制 JSON 输出格式。我实测下来如果只是接口返回格式问题用 Jackson 注解更简单JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;这样就不用动 MongoCustomConversions减少出错面。配置类保持最小化只在你确实需要自定义类型映射时才扩展。到这里连接和基础配置就绪。下一节开始写 CRUD 接口我会把 Controller 和 Service 分层方便你直接套用到项目里。3. 可复制的 CRUD 与分页接口MongoTemplate 增删改查完整代码这一节是核心直接上代码。我按 Controller Service 分层写Service 里封装 MongoTemplate 操作Controller 只做参数接收和响应包装。先看 Service。package com.example.mongo.service; import com.example.mongo.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; Service public class UserService { Autowired private MongoTemplate mongoTemplate; // 增 public User insert(User user) { user.setCreateTime(LocalDateTime.now()); return mongoTemplate.insert(user); } // 查单个 public User getById(String id) { return mongoTemplate.findById(id, User.class); } // 删 public boolean deleteById(String id) { Query query Query.query(Criteria.where(_id).is(id)); return mongoTemplate.remove(query, User.class).getDeletedCount() 0; } // 改只更新指定字段 public boolean updateField(String id, String userName, Integer age) { Query query Query.query(Criteria.where(_id).is(id)); Update update new Update(); if (userName ! null) { update.set(userName, userName); } if (age ! null) { update.set(age, age); } return mongoTemplate.updateFirst(query, update, User.class).getModifiedCount() 0; } // 分页 动态条件 public MapString, Object page(String userName, Integer minAge, int page, int size) { Query query new Query(); if (userName ! null !userName.isEmpty()) { query.addCriteria(Criteria.where(userName).regex(userName, i)); } if (minAge ! null) { query.addCriteria(Criteria.where(age).gte(minAge)); } long total mongoTemplate.count(query, User.class); query.with(Sort.by(Sort.Direction.DESC, createTime)); query.skip((long) (page - 1) * size).limit(size); ListUser data mongoTemplate.find(query, User.class); MapString, Object result new HashMap(); result.put(data, data); result.put(total, total); result.put(page, page); result.put(size, size); result.put(pages, (total size - 1) / size); return result; } }几个关键点解释一下。第一count必须在skip/limit之前执行否则 count 会被分页参数影响返回的是当前页条数而不是总数。第二skip的参数是 long(page - 1) * size如果 page 很大可能溢出 int所以强转 long 更稳。第三排序放在分页之前设置query.with(Sort)会覆盖之前的排序多次调用只保留最后一次。再看 Controller把接口暴露出去。package com.example.mongo.controller; import com.example.mongo.entity.User; import com.example.mongo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; RestController RequestMapping(/user) public class UserController { Autowired private UserService userService; PostMapping(/insert) public User insert(RequestBody User user) { return userService.insert(user); } GetMapping(/get/{id}) public User getById(PathVariable String id) { return userService.getById(id); } DeleteMapping(/delete/{id}) public boolean delete(PathVariable String id) { return userService.deleteById(id); } PutMapping(/update/{id}) public boolean update(PathVariable String id, RequestParam(required false) String userName, RequestParam(required false) Integer age) { return userService.updateField(id, userName, age); } GetMapping(/page) public MapString, Object page(RequestParam(required false) String userName, RequestParam(required false) Integer minAge, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { return userService.page(userName, minAge, page, size); } }这里Criteria的包是org.springframework.data.mongodb.core.query.Criteria不是com.mongodb.client.model.Filters导错包编译能过但运行时报方法找不到这是新手最常见的错误之一。关于insert和save的区别insert如果主键已存在会抛 DuplicateKeyExceptionsave则是存在就更新、不存在就插入。做新增接口用insert更安全能防止误覆盖做「有则更新无则插入」的场景用save。更新操作用Update对象而不是整个save好处是只更新指定字段不会把其他字段覆盖成 null。updateFirst只更新第一条匹配updateMulti更新所有匹配。返回的UpdateResult里getModifiedCount()是实际修改条数getMatchedCount()是匹配条数判断更新是否成功要看 modifiedCount。分页接口返回结构里我加了pages总页数前端做分页控件直接用它不用自己算。total是总记录数data是当前页列表。这个结构基本能覆盖大部分后台列表需求。代码写完了下一节讲怎么把 AI 辅助工具的 endpoint 统一到 TaoToken管理 Key 的同时让编码助手也能用上统一入口。4. TaoToken 统一 Key 配置把 AI 辅助工具的 endpoint 收口到一处做后端开发时很多人会同时用多个 AI 编码工具比如 Claude Code、Cline、Codex 这类。每个工具都要单独配 Key、单独记 endpoint换机器或者团队协作时特别乱。TaoToken 的思路是提供一个统一的 API 入口把 Key 管理和模型调用收口到一处你只需要维护一份配置。TaoToken 官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置时直接用这个。先说清楚它解决什么问题你有一堆 AI 工具每个都要填 Base URL 和 API Key工具之间 Key 不互通某个 Key 泄露了要一个个改。统一到 TaoToken 后Base URL 都指向同一个入口Key 在控制台统一管理换 Key 只改一处。配置分三步拿 Key、改 endpoint、验证。第一步登录控制台创建 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面新建一个 Key复制保存。这个 Key 就是后面所有工具共用的凭证。第二步改各个工具的 endpoint。以 Claude Code 为例它的配置在 settings.json 里路径通常是~/.claude/settings.json。你需要把 Base URL 指向 TaoToken 的 API 地址并填入刚才的 Key。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥 } }如果你用的是 Cline 这类 VS Code 插件配置在插件的 settings 里选择 Anthropic 兼容模式Base URL 填https://taotoken.net/apiAPI Key 填 TaoToken 的 KeyModel ID 填你要用的模型名比如claude-sonnet-4-20250514。这三件套——Base URL、Key、Model ID——缺一不可少填一个就会报 401 或 model not found。Codex 的配置在~/.codex/auth.json结构类似{ base_url: https://taotoken.net/api, api_key: sk-你的TaoToken密钥 }这里要提醒一句不同工具对 Base URL 的路径要求可能不同。有的工具会自动在 Base URL 后面拼/v1/messages有的要求你直接填到/v1。TaoToken 的 API 地址是https://taotoken.net/api如果工具报 404先检查是不是路径拼接问题把 Base URL 改成https://taotoken.net/api/v1试试。第三步验证配置生效。最简单的办法是在工具里发一条测试消息看能不能正常返回。如果报 401说明 Key 不对或没生效如果报 local proxy failed说明工具在走本地代理需要关掉代理设置如果报 reading choices 相关错误通常是返回格式和工具预期不匹配检查 Model ID 是否填对。配置文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各工具的详细接入步骤。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 可以先在网页上试模型是否可用再去配工具。如果你长期做编码和 Agent 任务可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 适合高频调用场景。配置完成后你的 SpringBoot 项目本身不需要改任何代码TaoToken 是给 AI 辅助工具用的和 MongoDB 连接是两回事。但把工具 endpoint 统一后你写 MongoTemplate 代码时让 AI 辅助补全、排错会更顺Key 也不用到处散落。5. 验证请求与常见报错排查401、local proxy failed、reading choices 怎么解配置写完必须验证不然等于没配。这一节先给验证步骤再对照真实报错逐个排查。验证分两层先验证 MongoDB 接口本身通不通再验证 TaoToken 配置生效。MongoDB 接口验证用 curl 直接打。先插一条数据curl -X POST http://localhost:8080/user/insert \ -H Content-Type: application/json \ -d {userName:zhangsan,password:123456,age:25}返回应该是一个带 id 的 JSON。拿到 id 后查单个curl http://localhost:8080/user/get/你的id再测分页curl http://localhost:8080/user/page?page1size10minAge20正常返回结构里total是总数data是列表pages是总页数。如果total是 0 但数据库里明明有数据检查集合名是否对得上——Document(collection t_user)对应数据库里的t_user集合MongoDB 集合名大小写敏感。TaoToken 配置验证在配好的工具里发一条消息比如让 Claude Code 解释一段代码。能正常返回就说明 Base URL、Key、Model ID 三件套都对。下面是我实际遇到过的报错和排查路径。报错一401 Unauthorized。原因通常是 Key 没填对、Key 过期、或者 Base URL 和 Key 不匹配。排查顺序先确认 Key 是从 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 复制的完整字符串没有多余空格再确认 Base URL 是https://taotoken.net/api没有拼错最后确认工具读的是你改的那个配置文件有些工具会读环境变量覆盖配置文件检查一下 shell 里有没有旧的ANTHROPIC_API_KEY。报错二local proxy failed。这个报错说明工具在尝试走本地代理但代理没起来或者端口不对。检查工具的代理设置把 proxy 相关配置清空让它直连 Base URL。如果你之前配过 HTTP_PROXY 环境变量也要清掉。报错三reading choices 相关错误。这类错误通常是返回的 JSON 结构和工具预期的不一致常见于 Model ID 填错或者工具版本和 API 格式不匹配。先确认 Model ID 是 TaoToken 支持的模型名再检查工具版本是否过旧。如果是 Cline升级到最新版通常能解决。报错四OAuth 相关错误。有些工具默认走 OAuth 登录流程不走 API Key。你需要在工具设置里切换到 API Key 模式填 Base URL 和 Key而不是点登录按钮。Claude Code 如果报 OAuth 错误检查 settings.json 里是不是同时存在 OAuth token 和 API Key两者冲突时以 OAuth 优先需要把 OAuth 相关字段删掉。报错五MongoTemplate 查询返回空但数据库有数据。检查Field注解的字段名和数据库实际 key 是否一致。MongoDB 是大小写敏感的userName和username是两个不同的字段。用 MongoDB 客户端db.t_user.findOne()看一下实际存储的 key 名。报错六分页 total 对但 data 少一条。检查skip计算(page - 1) * size当 page 从 1 开始时第一页 skip 0正确。如果 page 从 0 开始传第一页会 skip 成负数MongoDB 会报错或返回异常结果。统一约定 page 从 1 开始。排查时有个通用技巧先把配置简化到最小可用只填 Base URL 和 KeyModel ID 用默认值跑通后再加自定义项。这样能快速定位是哪一项配置出的问题。6. 把 Key 收口之后MongoTemplate 项目里值得留下的几个习惯接口跑通、TaoToken 配好之后有几个习惯能让你的 MongoDB 数据访问层更稳。第一分页查询永远先 count 再 find且用同一个 Query 对象。我见过有人 count 用一个 Query、find 用另一个条件不一致导致 total 和 data 对不上。正确做法是构建好 Query 后先 count再追加排序和分页参数然后 find。第二动态条件用query.addCriteria按需追加不要用字符串拼查询。MongoTemplate 的 Criteria 是类型安全的拼字符串容易注入且难维护。第三更新操作用Update对象指定字段不要整个save。save会把实体里为 null 的字段也写进去覆盖掉数据库里原有的值。只更新需要改的字段用update.set。第四给常用查询字段加Indexed。分页接口如果按 userName 过滤没索引的话数据量一大就慢。auto-index-creation: true配合Indexed注解启动时自动建索引。第五AI 辅助工具的 Key 统一到 TaoToken 后团队协作时新人只需要拿一个 Key、改一处 Base URL不用挨个工具配。配置文档和 API Keys 页面收藏好换机器时直接照抄。最后留一个实用技巧MongoDB 的_id查询用Criteria.where(_id).is(id)不要用Criteria.where(id).is(id)。数据库里存的是_idJava 实体里叫idMongoTemplate 会自动映射但手写 Criteria 时必须用数据库字段名_id否则查不到。代码都在上面了直接复制到项目里改包名就能跑。分页接口的返回结构可以根据你前端的分页组件调整字段名核心逻辑不变。