☰
NodeJS博客实战19:博客内容修改和删除的接口设计与实现
2026/10/8 12:21:18 网站建设 项目流程

1. 从一次“删错文章”说起:NodeJS 博客内容修改与删除接口到底解决什么问题

做 NodeJS 博客系统到第 19 篇,前端展示、分类、分页基本都跑通了,真正让人头疼的往往是后台那两条接口:内容修改和内容删除。我试过在本地把一篇测试文章删掉,结果因为接口没做参数校验,id传空直接把整张表的第一条记录干掉了,那一刻才意识到——博客内容修改接口和内容删除接口,不是“能跑就行”,而是必须把参数校验、数据库更新、删除确认三件事做扎实。

这一篇聚焦的就是 NodeJS 博客后台的这两个核心接口:/admin/content/edit和/admin/content/delete。它适合已经用 Express + Mongoose 搭好博客骨架、能正常发布文章,但编辑和删除还在用“手写 SQL 或直接改数据库”的开发者。你会看到完整的路由定义、findOne与populate联表查询、updateOne更新、deleteOne删除,以及用 curl 和 Postman 验证的完整流程。

先说清楚这两个接口分别能做什么:

  • 内容修改接口:接收文章id,先查出原内容渲染到编辑页,提交后校验标题和正文非空,再更新数据库。
  • 内容删除接口:接收文章id,校验存在性后执行删除,返回成功页或 JSON。

适合谁?适合正在跟做 NodeJS 博客实战、卡在“编辑页分类选中状态不对”或“删除后列表没刷新”的同学。下面所有代码都可以直接复制到你的routers/admin.js里,路径和原文保持一致。

在进入代码之前,先明确一个工程习惯:修改和删除都属于写操作,写操作必须做参数校验和结果反馈。很多教程只给Content.update就结束了,但真实项目里,id为空、id格式非法、文章不存在这三种情况都要有明确处理,否则线上就是灾难。

2. TaoToken 前置准备:给博客后台接一个稳定的模型能力入口

你可能会问,写博客的增删改查接口,跟 TaoToken 有什么关系?关系在于:现代博客后台早就不只是“存文章”了。内容修改接口里,你很可能想加一个“AI 润色标题”“自动生成摘要”的按钮;删除接口里,你可能想加“删除前让模型判断是否误删”。这些能力都需要一个稳定的模型调用入口。

TaoToken 在这里扮演的角色,是统一的模型 API 网关。它把不同模型的调用收敛成一套兼容 OpenAI 风格的接口,你不需要为每个模型单独维护一套 SDK 和鉴权逻辑。对博客项目来说,这意味着你可以在routers/admin.js里加一个/admin/content/polish路由,调用模型对话接口来润色文章,而不用改动现有的 Express 结构。

官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数,直接用于代码里的baseURL。

前置准备分三步:

第一步,拿到 API Key。进入控制台的 API Keys 页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ),创建一个新的 Key。这个 Key 只显示一次,复制后放到项目的.env文件里,不要硬编码进routers/admin.js。

第二步,确认模型 ID。在模型对话页面(https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite )可以看到当前可用的模型列表。博客润色场景一般选通用对话模型即可,把模型 ID 记下来,后面配置里要用。

第三步,理解接入文档。接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite )里说明了请求格式、鉴权头和返回结构。核心就是三件套:Base URL + API Key + Model ID。这三样在后面的配置片段里会反复出现。

如果你只是想让博客的增删改查接口先跑起来,TaoToken 这一步可以先跳过,等接口稳定后再加 AI 能力。但如果你打算长期做内容型项目,建议一开始就把模型调用层抽象出来,避免后期到处改代码。

对于需要长期编码、跑 Agent 任务的场景,可以了解 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ),它更适合持续性的开发工作流。而单纯的模型验证和调试,用模型对话页面就够了。

3. 可复制配置:Express 路由 + Mongoose 操作 + 模型调用三件套

这一节是全文的核心,给出可以直接复制到项目里的配置和代码。路径与原文一致:路由写在routers/admin.js,模板写在views/admin/content_edit.html。

3.1 内容修改接口:查询 + 渲染 + 提交保存

先看查询和渲染部分。这里用findOne根据id查文章,用populate('category')联表查出分类对象,再把分类列表和当前文章一起传给模板:

// routers/admin.js // 内容修改页面渲染 router.get('/content/edit', function (req, res) { var id = req.query.id || ''; var categories = []; Category.find().sort({ _id: 1 }).then(function (result) { categories = result; return Content.findOne({ _id: id }).populate('category'); }).then(function (content) { if (!content) { res.render('admin/error', { userInfo: req.userInfo, message: '指定内容不存在' }); return Promise.reject(); } res.render('admin/content_edit', { userInfo: req.userInfo, categories: categories, content: content }); }).catch(function (err) { console.error('内容修改查询失败:', err); res.render('admin/error', { userInfo: req.userInfo, message: '查询内容失败' }); }); });

注意这里有个容易踩的坑:Content.findOne({ _id: id })如果id是空字符串或非法格式,Mongoose 会抛CastError。所以生产环境建议先做一次mongoose.Types.ObjectId.isValid(id)校验。我在本地测试时,传id=abc直接让进程报错,后来加了校验才稳定。

再看提交保存部分。原文用的是Content.update,但新版 Mongoose 已经废弃了这个方法,推荐用updateOne或findByIdAndUpdate。这里给出兼容写法,并补上参数校验:

// routers/admin.js // 内容修改提交保存 router.post('/content/edit', function (req, res) { var id = req.query.id || ''; if (!id) { res.render('admin/error', { userInfo: req.userInfo, message: '缺少内容 ID' }); return; } if (req.body.title === '') { res.render('admin/error', { userInfo: req.userInfo, message: '标题不能为空' }); return; } if (req.body.content === '') { res.render('admin/error', { userInfo: req.userInfo, message: '内容不能为空' }); return; } Content.updateOne({ _id: id }, { category: req.body.category, title: req.body.title, desciption: req.body.desciption, content: req.body.content }).then(function () { res.render('admin/success', { userInfo: req.userInfo, message: '内容保存成功', url: '/admin/content/edit?id=' + id }); }).catch(function (err) { console.error('内容保存失败:', err); res.render('admin/error', { userInfo: req.userInfo, message: '内容保存失败' }); }); });

这里有个细节:原文里字段名是desciption(拼写少了一个 r),模板里也是这个名字。为了跟原文保持一致,我保留了它。但如果你是新项目,建议改成description,否则后期维护容易混淆。

3.2 内容删除接口:校验 + 删除 + 反馈

删除接口最怕的就是“删了不该删的”。所以除了id校验,最好再加一层“文章是否存在”的判断:

// routers/admin.js // 内容删除 router.get('/content/delete', function (req, res) { var id = req.query.id || ''; if (!id) { res.render('admin/error', { userInfo: req.userInfo, message: '缺少内容 ID' }); return; } Content.findOne({ _id: id }).then(function (content) { if (!content) { res.render('admin/error', { userInfo: req.userInfo, message: '要删除的内容不存在' }); return Promise.reject(); } return Content.deleteOne({ _id: id }); }).then(function () { res.render('admin/success', { userInfo: req.userInfo, message: '删除成功', url: '/admin/content' }); }).catch(function (err) { console.error('内容删除失败:', err); res.render('admin/error', { userInfo: req.userInfo, message: '删除失败' }); }); });

如果你用的是老版本 Mongoose,deleteOne可能不存在,那就用Content.remove({ _id: id })。但新项目一律建议deleteOne或findByIdAndDelete。

3.3 模板里的分类选中逻辑

编辑页最容易出问题的地方,是分类下拉框没有正确选中当前文章所属的分类。原文用content.category._id.toString() == category._id.toString()来比较,这个写法是对的,因为populate之后content.category是一个对象,而categories里的每一项也是对象,直接==比较对象永远为 false,必须转成字符串比较。

<!-- views/admin/content_edit.html --> {% extends 'layout.html' %} {% block main %} <div class="a-userNav"> <span><a href="/">管理首页</a></span> <span> / </span> <span>内容修改 - {{content.title}}</span> </div> <form style="padding-left: 50px; margin-top: 20px;" method="post"> <div> <label for="category">分类:</label> <select name="category" id="category" style="width: 200px;"> {% for category in categories %} {% if content.category._id.toString() == category._id.toString() %} <option value="{{category.id}}" selected>{{category.name}}</option> {% else %} <option value="{{category.id}}">{{category.name}}</option> {% endif %} {% endfor %} </select> </div> <div> <label for="title">内容标题:</label> <input type="text" name="title" id="title" style="width: 200px;" value="{{content.title}}"> </div> <div> <label for="desc">简介:</label> <input type="text" name="desciption" id="desciption" style="width: 200px;" value="{{content.desciption}}"> </div> <div> <label for="content" style="vertical-align: top;">内容:</label> <textarea name="content" id="content" style="width: 200px;" rows="4">{{content.content}}</textarea> </div> <button type="submit">提交</button> </form> {% endblock %}

3.4 模型调用配置片段(可选)

如果你要在博客后台加 AI 润色,可以在项目根目录建一个config/ai.json,把三件套写进去:

{ "baseURL": "https://taotoken.net/api", "apiKey": "你的_API_KEY", "model": "你的_MODEL_ID" }

然后在routers/admin.js里读取这个配置,调用模型对话接口。注意baseURL用 API 地址,不要带 UTM 参数。API Key 建议从环境变量读取,不要提交到 Git。

4. 验证请求:用 curl 和 Postman 跑通修改与删除接口

代码写完了,怎么确认它真的能用?这一节给出完整的验证流程。假设你的博客跑在http://localhost:3000,并且后台已经登录(有 session)。

4.1 验证修改接口的查询部分

先用 curl 请求编辑页,看是否返回文章内容:

curl -X GET "http://localhost:3000/admin/content/edit?id=你的文章ID" \ -H "Cookie: 你的session cookie" \ -o edit_page.html

打开edit_page.html,搜索文章标题。如果能看到value="文章标题",说明findOne和populate都正常。如果返回的是“指定内容不存在”,检查id是否正确、数据库里是否有这条记录。

4.2 验证修改接口的提交部分

用 curl 模拟表单提交:

curl -X POST "http://localhost:3000/admin/content/edit?id=你的文章ID" \ -H "Cookie: 你的session cookie" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "category=分类ID&title=修改后的标题&desciption=修改后的简介&content=修改后的正文"

如果返回“内容保存成功”,再去数据库或列表页确认标题是否真的变了。这里有个常见问题:category传的是分类的_id,如果传错,Mongoose 会抛CastError。所以提交前最好在前端用select的value绑定正确的_id。

4.3 验证删除接口

删除接口是 GET 请求,直接 curl:

curl -X GET "http://localhost:3000/admin/content/delete?id=你的文章ID" \ -H "Cookie: 你的session cookie"

返回“删除成功”后,去列表页确认文章是否消失。如果返回“要删除的内容不存在”,说明id对应的记录已经被删了,或者id格式不对。

4.4 用 Postman 验证

Postman 里建两个请求:

  • GEThttp://localhost:3000/admin/content/edit?id=xxx,在 Headers 里加Cookie。
  • POSThttp://localhost:3000/admin/content/edit?id=xxx,Body 选x-www-form-urlencoded,填category、title、desciption、content。

删除请求同理,用 GET 方法。Postman 的好处是可以保存这些请求,下次直接点 Send,不用每次敲 curl。

4.5 成功结果的判断标准

修改接口成功的标志:数据库里对应_id的文档,title、content、category字段都变成了新值,且category是有效的分类_id。

删除接口成功的标志:数据库里对应_id的文档消失,且列表页不再显示该文章。

如果修改后分类没变,检查req.body.category是否真的传了值;如果删除后列表还在,检查是不是缓存或者查询条件没刷新。

5. 本篇常见错误排查:401、CastError、分类不选中、删除无效

这一节对照真实报错,给出排查路径。每个错误都给出“现象—原因—解决”三段式。

5.1 报错CastError: Cast to ObjectId failed for value "xxx"

现象:请求/admin/content/edit?id=abc时,进程直接抛错,页面 500。

原因:id不是合法的 ObjectId 格式,Mongoose 在findOne时尝试转换失败。

解决:在查询前加校验:

const mongoose = require('mongoose'); if (!mongoose.Types.ObjectId.isValid(id)) { res.render('admin/error', { userInfo: req.userInfo, message: '内容 ID 非法' }); return; }

5.2 报错401 Unauthorized或local proxy failed

现象:调用模型接口时返回 401,或者提示local proxy failed。

原因:API Key 没传、传错,或者baseURL配置成了本地代理地址。

解决:检查三件套——Base URL 是否为https://taotoken.net/api,API Key 是否从控制台正确复制,Model ID 是否在模型列表里存在。如果用了环境变量,确认.env已加载。local proxy failed通常是baseURL指向了不存在的本地端口,改回官方 API 地址即可。

5.3 报错Cannot read property 'choices' of undefined

现象:解析模型返回时,读response.choices[0]报错。

原因:返回结构不是预期的 OpenAI 格式,或者请求本身失败了但没检查状态码。

解决:先打印完整返回体,确认choices字段是否存在。如果返回的是错误信息,先处理错误分支。不要直接假设choices一定存在。

5.4 编辑页分类没有选中当前分类

现象:打开编辑页,下拉框默认选中第一个分类,而不是文章原本的分类。

原因:模板里比较的是对象而不是字符串,或者populate没生效。

解决:确认content.category是对象(有_id和name),比较时用.toString()。如果content.category是字符串 ID,说明populate没写或写错,检查Content.findOne({ _id: id }).populate('category')。

5.5 删除接口返回成功但数据还在

现象:curl 返回“删除成功”,但列表页文章还在。

原因:可能是deleteOne的查询条件没匹配到,或者前端有缓存。

解决:先直接查数据库确认记录是否真的删了。如果没删,检查id是否传对;如果删了但页面还在,检查列表查询是否有缓存或分页问题。

5.6 OAuth 相关报错

如果你在博客后台接了第三方登录,可能会遇到 OAuth 回调失败。这类问题一般跟接口本身无关,检查回调地址、Client ID、Client Secret 是否配置正确。如果错误信息里出现OAuth字样,先隔离登录模块,确认增删改查接口本身是通的。

6. 继续往下走:把接口用起来,而不是停在复制粘贴

写到这里,修改和删除接口的代码、配置、验证、排障都齐了。最后说点实在的:接口写完只是开始,真正让博客稳定的是你对边界情况的处理。

比如,删除接口现在是 GET 请求,这意味着任何人在浏览器地址栏输入/admin/content/delete?id=xxx都能触发删除。生产环境应该改成 POST,并加 CSRF 防护。再比如,修改接口没有做权限校验,任何登录用户都能改别人的文章,实际项目里要加author字段比对。

如果你想让博客后台更智能,可以在修改接口里加一个“AI 润色”按钮,调用模型对话接口(https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite )来优化标题和摘要。接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite )里有完整的请求示例。API Key 在控制台(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite )创建。

长期做内容项目的同学,可以看看 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ),它更适合持续性的开发和 Agent 任务。

下一篇我会写博客前台的搜索接口和标签筛选,把查询条件组合起来。你可以先把这一篇的代码跑通,遇到报错就对照第 5 节排查。接口这东西,跑通一次,后面都是肌肉记忆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询