Gin 前后端分离项目实战:从 0 到上线一个多端 Gin+Gorm项目
前言
最近为了系统练习 Go 后端开发,我做了一个完整的多端小项目:一间清单 Todo-list。
它不是一个只写几个接口的 Demo,而是把一个后端项目从功能设计、数据库建模、接口开发、前端联调,到最后部署上线完整走了一遍。项目本身不复杂,但覆盖了 Gin 入门到实战里非常常见的一套流程。
项目核心功能是:用户输入一个房间名称,进入独立的任务空间。在每个房间中,可以分别管理「学习清单」和「任务清单」。
项目地址:
https://github.com/tuoxie423/Todo-List.git在线体验:
https://list.tuoxie.asia小程序端:
开发中的微信小程序:一间清单为什么做这个项目
学习 Gin 的时候,如果只写单个接口,很容易停留在“会用”的阶段。真正做一个项目时,还会遇到很多实际问题:
- 路由怎么组织
- 表结构怎么设计
- 前后端怎么联调
- 配置文件怎么管理
- 数据怎么隔离
- 部署后 502 怎么排查
- 服务怎么后台稳定运行
所以我选择做一个小而完整的任务清单项目,用一个清晰的业务场景,把这些知识点串起来。
项目功能
这个项目主要实现了这些功能:
- 输入房间名称,创建或进入房间
- 每个房间拥有独立任务数据
- 管理学习清单
- 管理任务清单
- 新增任务
- 标记完成或恢复未完成
- 删除任务
- 浏览器端记录最近进入过的房间
- 网站端和小程序端共用同一套后端 API
整体可以理解为一个轻量级的多端任务清单系统。
项目结构
项目目录大致如下:
. ├── backend │ ├── main.go │ ├── config │ │ └── config.go │ ├── global │ │ └── global.go │ └── model │ ├── room.go │ └── task.go ├── frontend │ ├── home.html │ ├── home.js │ ├── tasks.html │ ├── app.js │ └── server.js ├── miniprogram │ ├── pages │ └── utils ├── config │ └── config.example.yaml └── README.md其中:
backend:Gin 后端服务frontend:浏览器端页面miniprogram:微信小程序端config:统一管理端口、数据库等配置
数据库模型设计
项目里有两个核心模型:房间和任务。
房间表
房间用于区分不同用户输入的任务空间。
typeRoomstruct{IDint`gorm:"primaryKey" json:"id"`Namestring`gorm:"type:varchar(100);not null;uniqueIndex" json:"name"`CreatedAt time.Time`json:"createdAt"`}对应数据库表可以理解为:
rooms --- id 主键 name 房间名称,唯一 created_at 创建时间这里给name加了唯一索引。这样用户再次输入同一个房间名时,会进入原来的清单,而不是创建一个重复房间。
任务表
任务表通过room_id和房间关联。
typeTaskstruct{IDint`gorm:"primaryKey" json:"id"`RoomIDint`gorm:"not null;index" json:"roomId"`Titlestring`gorm:"type:varchar(100);not null" json:"title"`Levelstring`gorm:"type:varchar(20);not null;default:'基础'" json:"level"`Kindstring`gorm:"type:varchar(30);not null;default:'learning'" json:"kind"`Donebool`gorm:"not null;default:false" json:"done"`CreatedAt time.Time`json:"createdAt"`}对应数据库表可以理解为:
tasks --- id 主键 room_id 所属房间 ID title 任务标题 level 任务难度 kind 任务类型:learning / optimization done 是否完成 created_at 创建时间这样设计之后,查询任务时必须带上room_id,可以保证不同房间之间的数据互不影响。
接口设计
后端接口按照 RESTful 风格设计:
POST /api/rooms GET /api/rooms/:roomID/tasks POST /api/rooms/:roomID/tasks PATCH /api/rooms/:roomID/tasks/:taskID/toggle DELETE /api/rooms/:roomID/tasks/:taskID创建或进入房间
POST /api/rooms请求体:
{"name":"Go 后端练习"}后端会先根据房间名查询。如果房间不存在就创建;如果已经存在,就直接返回已有房间。
查询房间任务
GET /api/rooms/:roomID/tasks返回:
{"items":[{"id":1,"roomId":1,"title":"跑通 Gin 后端服务","level":"基础","kind":"learning","done":false,"createdAt":"2026-06-27T..."}]}新增任务
POST /api/rooms/:roomID/tasks请求体:
{"title":"学习 GORM 表关联","level":"进阶","kind":"learning"}其中kind用来区分不同清单:
learning 学习清单 optimization 任务清单Gin 路由实现
后端使用 Gin 创建路由:
funcsetupRouter(db*gorm.DB)*gin.Engine{router:=gin.New()router.Use(gin.Logger(),gin.Recovery(),corsMiddleware())router.GET("/health",func(c*gin.Context){c.JSON(http.StatusOK,gin.H{"status":"ok","time":time.Now().Format(time.RFC3339),})})api:=router.Group("/api"){api.POST("/rooms",func(c*gin.Context){// 创建或进入房间})roomTasks:=api.Group("/rooms/:roomID/tasks"){roomTasks.GET("",func(c*gin.Context){// 查询任务})roomTasks.POST("",func(c*gin.Context){// 创建任务})roomTasks.PATCH("/:taskID/toggle",func(c*gin.Context){// 切换任务状态})roomTasks.DELETE("/:taskID",func(c*gin.Context){// 删除任务})}}returnrouter}这里把任务接口都放在:
/api/rooms/:roomID/tasks下面,是因为任务必须属于某一个房间。这样接口语义更清楚,也能避免误操作其他房间的数据。
参数校验
项目里对房间 ID 和任务 ID 做了统一校验:
funcparseParamID(c*gin.Context,namestring,messagestring)(int,bool){id,err:=strconv.Atoi(c.Param(name))iferr!=nil||id<=0{c.JSON(http.StatusBadRequest,gin.H{"message":message})return0,false}returnid,true}使用时很直接:
roomID,ok:=parseParamID(c,"roomID","房间 ID 必须是正整数")if!ok{return}除了 ID 校验,还要判断房间是否存在:
funcroomExists(c*gin.Context,db*gorm.DB,roomIDint)bool{varcountint64iferr:=db.Model(&model.Room{}).Where("id = ?",roomID).Count(&count).Error;err!=nil{c.JSON(http.StatusInternalServerError,gin.H{"message":"查询房间失败"})returnfalse}ifcount==0{c.JSON(http.StatusNotFound,gin.H{"message":"房间不存在"})returnfalse}returntrue}这样可以避免给不存在的房间添加任务。
创建任务流程
创建任务时,后端主要做了这些事:
- 解析
roomID - 判断房间是否存在
- 解析 JSON 请求体
- 校验任务标题不能为空
- 规范化难度和任务类型
- 写入数据库
- 返回创建后的任务数据
核心代码:
varpayload createTaskRequestiferr:=c.ShouldBindJSON(&payload);err!=nil{c.JSON(http.StatusBadRequest,gin.H{"message":"请求体必须是 JSON"})return}title:=strings.TrimSpace(payload.Title)iftitle==""{c.JSON(http.StatusBadRequest,gin.H{"message":"任务标题不能为空"})return}task:=model.Task{RoomID:roomID,Title:title,Level:normalizeLevel(payload.Level),Kind:normalizeKind(payload.Kind),}iferr:=db.Create(&task).Error;err!=nil{c.JSON(http.StatusInternalServerError,gin.H{"message":"创建任务失败"})return}c.JSON(http.StatusCreated,task)这里前端虽然也会做表单校验,但后端仍然必须校验。因为后端不能完全相信客户端传来的数据。
前端如何调用接口
前端通过fetch调用后端接口。
例如进入房间:
constroom=awaitrequest("/api/rooms",{method:"POST",body:JSON.stringify({name}),});新增任务:
consttask=awaitrequest(`/api/rooms/${currentRoom.id}/tasks`,{method:"POST",body:JSON.stringify({title,level:levelInput.value,kind,}),});前端页面本身不直接操作数据库,只负责收集用户输入,然后调用后端接口。真正的数据保存逻辑都在后端完成。
多端适配
这个项目除了浏览器端,还保留了微信小程序端目录。
小程序端和网站端共用同一套 API,只是请求方式不同:
浏览器端:fetch 小程序端:wx.request小程序「一间清单」目前仍在开发/发布中。等后端接口和网站稳定后,小程序只需要把接口域名配置为:
https://list.tuoxie.asia并在微信公众平台配置 request 合法域名即可。
配置文件管理
项目使用 YAML 文件统一管理配置。
示例:
backend:port:18080mode:releasefrontend:port:18090apiBase:http://localhost:18080database:host:localhostport:3306user:your_usernamepassword:your_passwordname:listcharset:utf8mb4真实配置文件不应该上传仓库,只上传示例文件:
config/config.example.yaml真实文件:
config/config.yaml应该加入.gitignore,避免数据库账号密码泄露。
跨域处理
前后端分离时,浏览器请求后端接口可能遇到跨域问题,所以后端加了一个简单的 CORS 中间件:
funccorsMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){c.Writer.Header().Set("Access-Control-Allow-Origin","*")c.Writer.Header().Set("Access-Control-Allow-Methods","GET,POST,PATCH,DELETE,OPTIONS")c.Writer.Header().Set("Access-Control-Allow-Headers","Content-Type")ifc.Request.Method==http.MethodOptions{c.AbortWithStatus(http.StatusNoContent)return}c.Next()}}练习项目这样写比较简单。正式项目可以根据具体域名收紧允许的来源。
部署上线
项目上线时,我使用 Nginx 对外提供 HTTPS,再反向代理到本机前后端服务:
https://list.tuoxie.asia ↓ Nginx 443 ↓ / -> 127.0.0.1:18090 /api/ -> 127.0.0.1:18080 /health -> 127.0.0.1:18080后端编译命令:
go build-buildvcs=false-otodo-list.服务后台运行建议使用systemd,这样进程异常退出后可以自动重启,也方便查看日志。
项目总结
这个项目虽然功能简单,但覆盖了 Go 后端开发中很常见的一套流程:
- Gin 路由分组
- RESTful API 设计
- JSON 参数解析
- 参数校验
- GORM 操作数据库
- 一对多表关联
- 前后端分离调用
- YAML 配置管理
- 网站部署上线
- 小程序多端适配
对初学者来说,它比单独写几个接口更有练习价值。因为它不是只关注某一个知识点,而是把一个后端项目从开发到上线的链路完整走了一遍。
后续还可以继续扩展:
- 增加用户登录
- 增加房间权限
- 增加任务编辑
- 增加分页查询
- 增加接口日志
- 增加更完整的单元测试
如果你也在学习 Gin,可以把这个项目当作一个练手模板:先跑通,再理解接口设计,最后尝试自己扩展功能。