首页/新闻资讯/正文详情

11 — 完整 CRUD 示例

发布时间:2026/9/24 17:28:48 来源:云帆数科 栏目:资讯中心
11 — 完整 CRUD 示例
11.1 这一篇做什么前面几篇分别讲了每一层的写法这一篇把它们串起来从零写完一个项目管理模块的完整 CRUD。读完这篇你应该能照着写出其他任何模块。11.2 要实现的接口方法URL功能GET/api/v1/projects分页查询项目列表支持按名称搜索GET/api/v1/projects/:id查询单个项目详情POST/api/v1/projects新建项目PUT/api/v1/projects/:id更新项目DELETE/api/v1/projects/:id删除项目统一响应格式{ status: 0, data: ... }11.3 第一步定义数据模型src/models/project/project.tsexportinterfaceProject{id:number;name:string;description:string;status:active|inactive;created_at:number;updated_at:number;}11.4 第二步写 Model 层src/models/project/project-model.tsimporttype{Project}from./project.js;exportclassProjectModel{#projects:Project[];constructor(initialData:readonlyProject[][]){this.#projectsinitialData.map((p)structuredClone(p));}list():Project[]{returnstructuredClone(this.#projects);}findById(id:number):Project|undefined{constprojectthis.#projects.find((p)p.idid);returnproject?structuredClone(project):undefined;}findByName(name:string):Project|undefined{constprojectthis.#projects.find((p)p.namename);returnproject?structuredClone(project):undefined;}create(input:PickProject,name|description|status):Project{constnowMath.floor(Date.now()/1000);constproject:Project{...input,id:this.#nextId(),created_at:now,updated_at:now,};this.#projects.push(structuredClone(project));returnstructuredClone(project);}update(id:number,input:PartialPickProject,name|description|status,):Project|undefined{constprojectthis.#projects.find((p)p.idid);if(!project)returnundefined;if(input.name!undefined)project.nameinput.name;if(input.description!undefined)project.descriptioninput.description;if(input.status!undefined)project.statusinput.status;project.updated_atMath.floor(Date.now()/1000);returnstructuredClone(project);}delete(id:number):boolean{constindexthis.#projects.findIndex((p)p.idid);if(index0)returnfalse;this.#projects.splice(index,1);returntrue;}#nextId():number{if(this.#projects.length0)return1;returnMath.max(...this.#projects.map((p)p.id))1;}}11.5 第三步准备初始数据src/data/project/index.tsimporttype{Project}from/models/project/project.js;constCREATED_AT1_700_000_000;exportconstprojectData:readonlyProject[][{id:1,name:示例项目A,description:这是第一个示例项目,status:active,created_at:CREATED_AT,updated_at:CREATED_AT,},{id:2,name:示例项目B,description:这是第二个示例项目,status:inactive,created_at:CREATED_AT,updated_at:CREATED_AT,},];11.6 第四步写 Service 层src/services/project/project-service.tsimport{AppError}from/errors/app-error.js;importtype{ProjectModel}from/models/project/project-model.js;importtype{Project}from/models/project/project.js;exportclassProjectService{constructor(privatereadonlyprojects:ProjectModel){}// 查询列表支持按名称模糊搜索list(name):Project[]{constallthis.projects.list();if(!name)returnall;constlowername.toLowerCase();returnall.filter((p)p.name.toLowerCase().includes(lower));}// 查询详情getById(id:number):Project{constprojectthis.projects.findById(id);if(!project){thrownewAppError(项目不存在,{status:2});}returnproject;}// 新建create(body:Recordstring,unknown):Project{constnamethis.#requireString(body.name,项目名称不能为空);constdescriptiontypeofbody.descriptionstring?body.description:;conststatusbody.statusinactive?inactive:active;if(this.projects.findByName(name)){thrownewAppError(项目名称已存在,{status:2});}returnthis.projects.create({name,description,status});}// 更新update(id:number,body:Recordstring,unknown):Project{constcurrentthis.projects.findById(id);if(!current){thrownewAppError(项目不存在,{status:2});}constnamebody.name!undefined?this.#requireString(body.name,项目名称不能为空):undefined;if(namename!current.name){if(this.projects.findByName(name)){thrownewAppError(项目名称已存在,{status:2});}}constupdatedthis.projects.update(id,{name,description:typeofbody.descriptionstring?body.description:undefined,status:body.statusinactive||body.statusactive?body.status:undefined,});if(!updated){thrownewAppError(更新失败,{status:1});}returnupdated;}// 删除delete(id:number):void{constcurrentthis.projects.findById(id);if(!current){thrownewAppError(项目不存在,{status:2});}constsuccessthis.projects.delete(id);if(!success){thrownewAppError(删除失败,{status:1});}}#requireString(value:unknown,errorMsg:string):string{conststrtypeofvaluestring?value.trim():;if(!str){thrownewAppError(errorMsg,{status:2});}returnstr;}}11.7 第五步写 Controller 层src/controllers/project/project-controller.tsimporttype{Request,Response}fromexpress;importtype{ProjectService}from/services/project/project-service.js;import{sendOk,sendList}from/utils/response.js;import{parseOffset,parseSize,parseText}from/utils/pagination.js;exportclassProjectController{constructor(privatereadonlyservice:ProjectService){}list(req:Request,res:Response){constoffsetparseOffset(req.query.offset);constsizeparseSize(req.query.size);constnameparseText(req.query.name);constallthis.service.list(name);returnsendList(res,all.slice(offset,offsetsize),{count:all.length,offset,size,});};get(req:Request,res:Response){constprojectthis.service.getById(Number(req.params.id));returnsendOk(res,project);};create(req:Request,res:Response){constprojectthis.service.create(req.body);returnsendOk(res,{id:project.id});};update(req:Request,res:Response){this.service.update(Number(req.params.id),req.body);returnsendOk(res,null);};remove(req:Request,res:Response){this.service.delete(Number(req.params.id));returnsendOk(res,null);};}11.8 第六步写 Router 层src/routers/project/project-router.tsimport{Router}fromexpress;importtype{ProjectController}from/controllers/project/project-controller.js;exportfunctioncreateProjectRouter(controller:ProjectController){constrouterRouter();router.get(/,controller.list);// GET /projectsrouter.get(/:id,controller.get);// GET /projects/:idrouter.post(/,controller.create);// POST /projectsrouter.put(/:id,controller.update);// PUT /projects/:idrouter.delete(/:id,controller.remove);// DELETE /projects/:idreturnrouter;}11.9 第七步注册到总路由src/bootstrap.ts里加上import{projectData}from./data/project/index.js;import{ProjectModel}from./models/project/project-model.js;import{ProjectService}from./services/project/project-service.js;import{ProjectController}from./controllers/project/project-controller.js;exportfunctionbootstrap(){constprojectsnewProjectModel(projectData);constprojectnewProjectController(newProjectService(projects));return{projects,controllers:{project},};}src/routers/index.ts里加上import{createProjectRouter}from./project/project-router.js;exportfunctioncreateRouter(controllers:any){constrouterRouter();router.use(/projects,createProjectRouter(controllers.project));returnrouter;}11.10 测试一下启动服务npmrun dev用 curl 测试# 列表curlhttp://localhost:3000/api/v1/projects# 搜索curlhttp://localhost:3000/api/v1/projects?nameA# 详情curlhttp://localhost:3000/api/v1/projects/1# 新建curl-XPOST http://localhost:3000/api/v1/projects\-HContent-Type: application/json\-d{name:新项目,description:测试,status:active}# 更新curl-XPUT http://localhost:3000/api/v1/projects/3\-HContent-Type: application/json\-d{name:改名后的项目}# 删除curl-XDELETE http://localhost:3000/api/v1/projects/311.11 小结一个完整的 CRUD 模块需要 6 个文件文件作用models/project/project.ts类型定义models/project/project-model.ts数据操作data/project/index.ts初始数据services/project/project-service.ts业务逻辑controllers/project/project-controller.ts请求处理routers/project/project-router.ts路由映射每一层职责单一改其中一层不影响其他层。照着这个模板写其他模块就行。

相关推荐

报告下载丨2025 年 AI Agent 重新定义未来企业:微软集团生产力与企业流程业务研究报告
报告下载丨2025 年 AI Agent 重新定义未来企业:微软集团生产力与企业流程业务研究报告

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

Java27新鲜出炉!IDEA已满血适配,保姆级上手指南
Java27新鲜出炉!IDEA已满血适配,保姆级上手指南

虽然Java27不是LTS,但性能、安全、新语法全都塞给你,打工人快来尝鲜 每隔半年,Java就准时扔出一个新版本,2026年9月15日,Java 27正式上线,一共9个JEP:4个正式定稿特性、4个预览特性,… · 2026/9/24 17:28:48

09 — Router 层:路由配置
09 — Router 层:路由配置

9.1 Router 层是什么 Router 层负责把 URL 和 HTTP 方法映射到对应的 Controller 方法。 Express 的 Router 可以把路由分组管理,而不是把所有路由都堆在 app.ts 里。9.2 基本用法 最简单的写法 import { Router } from express; import { ProjectController } from… · 2026/9/24 17:28:42

MATLAB高斯过程回归:置信区间计算与可视化详解
MATLAB高斯过程回归:置信区间计算与可视化详解

我最初接触MATLAB里的高斯过程回归时,网上能找到的资料大多是零碎的demo,真正能讲清楚“为什么这么写”“置信区间那条带子到底怎么来的”的并不多。结果就是很多人用fitrgp跑通了一版预测曲线,可一旦要解释模型在各区域的可靠程度、要判断拟… · 2026/9/24 18:44:03

Dockerfile镜像分层机制与高效构建实战
Dockerfile镜像分层机制与高效构建实战

1. 从一份构建脚本说起:Dockerfile到底在解决什么问题 第一次接触容器化的时候,不少人会问我一个问题:“我明明已经写好了一个代码仓库,为什么还要再折腾一个Dockerfile?”这个问题的答案,得从“环境一致性… · 2026/9/24 18:44:03

个人微信API二次开发:如何实现微信消息自动收发?
个人微信API二次开发:如何实现微信消息自动收发?

业务系统要给客户发通知、也要听客户回什么,一查开放平台就明白:个人微信会话没有给你用的官方收发 API。个人微信API二次开发走的是另一条路——账号扫码登录成执行节点,发信用 HTTP,收走 Webhook,两边都进你自己的服… · 2026/9/24 18:44:03

Windows下用MSYS2+MinGW64编译FFmpeg并集成x264/x265完整指南
Windows下用MSYS2+MinGW64编译FFmpeg并集成x264/x265完整指南

1. 为什么需要自己编译 FFmpeg:三个绕不开的理由很多人在 Windows 下用到 FFmpeg,第一反应是去官网下载编译好的 exe 包,解压丢进 PATH 就完事了。这个路子应付日常转码确实够用,但一旦你开始做这几件事,现成包就顶不住… · 2026/9/24 18:44:03

扫雷数字显示:Flutter for OpenHarmony游戏实战解析
扫雷数字显示:Flutter for OpenHarmony游戏实战解析

我最早接触扫雷还是在PC上,那时候的Windows系统自带游戏,简简单单一个格子面板却总能让人一玩就是一下午。后来做移动端开发,接触了Flutter,又陆续研究OpenHarmony的生态,一个念头就很自然地冒出来:能不能把… · 2026/9/24 18:44:03

从2比10到25比23:中国女排用23天完成一场漂亮的翻身仗
从2比10到25比23:中国女排用23天完成一场漂亮的翻身仗

2比10落后,还能赢吗? 9月22日晚,2026年爱知名古屋亚运会女排决赛,中国女排在第三局开局落后8分的情况下,将比分追至19平,最终以25比23完成逆转。随着日本队最后一次接发球出界,中国女排以3比0击… · 2026/9/24 18:43:57

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码