做第一个全栈项目的过程就像第一次独立装修一套房子每一道工序都要自己上手每一步都可能踩到意想不到的坑。我选的题目是任务管理器一个看似简单、实际涵盖了用户体系、增删改查、状态流转、多端适配的经典业务系统。用Vue 3做Web端Golang的Gin框架写后端接口数据层交给MySQL顺便用uniapp规划了移动端的扩展方向——这算是目前中小型项目里非常标准的一套全栈组合了。这篇复盘既是对我自己开发过程的记录也是给同样准备独立做全栈项目的朋友一份参考。从需求拆解、技术选型、数据库设计、接口约定到前端页面落地、联调部署、问题排查我会把整个过程里值得说的地方都写出来特别是那些只有亲自踩过坑才能总结出来的细节。无论你是想用这个项目练手还是已经在规划自己的第一个全栈作品这篇内容应该都能帮你在动工之前把思路理得更清楚。1. 项目概览这个任务管理器到底做了什么1.1 为什么第一个全栈项目选任务管理器技术圈里有种普遍认知想快速成长就去做一个“麻雀虽小、五脏俱全”的业务系统。任务管理器恰好是这类系统的典型代表。它不像电商那么庞大不需要处理支付、库存、物流这些复杂域但又保留了真正的全栈项目必须具备的核心模块——用户注册登录、数据建模、增删改查、状态流转、列表筛选、分页排序。这些功能做一遍整个前后端开发的主干流程就算完整走通了。还有一个很现实的原因任务管理器的需求边界非常清晰。你可以把一个功能完善的任务管理器做出来也可以只做核心的待办事项。它的复杂度完全由你自己掌控不会出现需求发散、越做越没边的情况。我给自己定的版本范围很明确支持用户注册登录、任务的新增删除修改查询、按状态和优先级筛选、按截止时间和创建时间排序、基础的数据统计。这个范围做下来刚好能把一个全栈工程师需要具备的基本能力全部覆盖到。1.2 技术栈选型为什么是Vue 3 Golang uniapp技术栈的选型我做了一些实际的对比。前端框架首选Vue 3原因很直接Composition API对逻辑复用特别友好一个任务的增删改查逻辑可以封装成独立的组合式函数组件之间共享状态非常干净。加上TypeScript的类型提示后端的接口数据结构能直接映射到前端类型定义联调的时候省了大量对照文档的时间。组件库用的Element Plus表格、表单、日期选择器这些都是现成的开发速度很快。后端选择Golang主要是看中三点第一编译型语言部署极简单一个二进制文件扔到服务器上就能跑不像Node.js项目要处理一堆依赖安装第二性能足够好虽然任务管理器的并发量不大但学习过程中用到的goroutine、channel这些并发模型对后续进阶帮助很大第三Gin框架的中间件机制清晰JWT认证、日志记录、错误处理这些横切关注点可以非常优雅地组织起来。数据库用MySQL这是最通用的关系型数据库选择GORM作为ORM工具能大幅减少手写SQL的工作量。提到uniapp我承认这是有一点点“私心”的规划。任务管理器这种工具类应用很多人希望在手机上顺手记一笔。如果用纯H5做移动端适配在iPhone的刘海屏和Android的各种异形屏上调试会让人崩溃。uniapp至少保留了后续打包成App的可能性。不过在第一个版本里我没有完整实现移动端而是把核心逻辑全部收敛在前后端API层Web端用响应式布局适配移动设备。这个取舍后面会细说。1.3 功能清单与版本规划动工之前我写了一页纸的产品需求清单标注了“必须做”和“以后做”两个维度。这样做的好处是开发过程中不会被临时冒出来的想法带偏。第一个版本必须包含的功能项用户注册、登录、退出登录后返回JWT令牌任务的创建、查询、修改、删除任务字段标题、描述、状态、优先级、标签、截止时间任务列表支持按状态、优先级筛选支持模糊搜索、分页、排序任务状态流转待办 → 进行中 → 已完成个人任务统计数据总数、已完成数、进行中数、过期未完成数暂缓的功能包括任务的子任务拆分、提醒通知、日历视图、多人协作、数据导出。这些功能不是不重要而是在第一个版本引入会破坏核心链路的完整性。比如多人协作一旦做了就要考虑共享、权限、冲突处理这会直接把开发周期拉长一倍以上。全栈项目的第一版最重要的原则是完成闭环而不是功能齐全。2. 架构设计与数据模型2.1 前后端分离架构与目录结构我采用的是经典的前后端分离架构Vue 3前端只负责界面渲染和用户交互Golang后端只提供JSON格式的API接口两者通过HTTP通信。前端运行在开发服务器上通过Vite的代理把/api前缀的请求转发到后端的端口生产环境则由Nginx统一托管前端静态文件和反向代理后端接口。后端项目的目录结构是参考了Gin项目的社区最佳实践按职责分模块task-manager-server/ ├── main.go // 入口文件加载配置、初始化数据库、注册路由 ├── config/ │ ├── config.go // 配置结构体定义 │ └── config.yaml // 环境配置 ├── models/ │ ├── user.go // 用户模型 │ └── task.go // 任务模型 ├── middlewares/ │ ├── jwt_auth.go // JWT认证中间件 │ ├── cors.go // 跨域处理 │ └── logger.go // 请求日志 ├── controllers/ │ ├── auth_controller.go // 认证相关接口 │ └── task_controller.go // 任务相关接口 ├── services/ │ ├── auth_service.go // 认证业务逻辑 │ └── task_service.go // 任务业务逻辑 ├── dto/ │ ├── auth_dto.go // 请求/响应数据结构 │ └── task_dto.go ├── routes/ │ └── router.go // 路由注册 ├── utils/ │ ├── jwt.go // JWT工具函数 │ └── response.go // 统一响应封装 └── database/ └── db.go // 数据库初始化连接这种分层的好处每个关注点各归其位控制器只做参数接收和数据返回业务逻辑全部下沉到service层数据模型在models层定义。对新手来说最大的收益是代码结构一目了然出问题的时候能快速定位——接口报错先看controller逻辑不对查service字段对不上看models。前端的目录结构相对简单一些按页面(views)、组件(components)、状态(store)、路由(router)、API封装(api)五个维度组织。API层单独抽出来的好处是所有的后端请求地址都集中在一个文件里后端接口路径变了只需要改一处不用满项目搜索。2.2 数据库表设计与状态流转数据库总共就两张表设计得很克制。用户表只保留了核心字段任务表除了基本字段外在建索引上做了思考。直接看建表语句CREATE TABLE users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tasks ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待办 1进行中 2已完成, priority TINYINT NOT NULL DEFAULT 1 COMMENT 1低 2中 3高, tag VARCHAR(50), due_date DATETIME, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_status (user_id, status), KEY idx_user_due_date (user_id, due_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段用TINYINT表示状态和优先级而不是直接用字符串很多人不太理解这点。从数据库角度看整数型的查询效率更高存储空间更小从代码角度看状态定义成常量后前端只需要映射数字对应的文案和标签颜色就行。比如status 2就是已完成配合TypeScript的联合类型写起来相当舒服。状态流转我实现了一个非常轻量的状态机。后端在更新状态时检查合法迁移路径待办只能流转到进行中进行中只能流转到已完成。看起来只是一个简单的switch判断但这个问题反映了一个设计思维——不是所有操作都应该被允许业务规则要放在后端用代码保护不能只靠前端控制。字符集用utf8mb4是因为任务标题和标签可能出现表情符号比如客户想给某个任务加个 标记。utf8mb4是完整的UTF-8编码能存emoji而且和旧版utf8在大部分场景下完全兼容。2.3 接口约定与统一响应格式前后端联调最怕的就是接口各写各的所以在开发后端之前我把接口规范先定死了。所有接口统一使用/api/v1前缀版本号直接放在路径里后续如果有不兼容的改动可以平滑升级到v2而不用影响已上线的客户端。响应格式统一封装成一个JSON结构{ code: 0, message: ok, data: { } }code为0表示成功非0表示业务错误。业务错误码我稍微做了分类前缀不同方便排查100开头是认证相关错误比如token过期、无效token200开头是参数校验错误300开头是资源操作错误比如任务不存在、无权限修改。这种错误码分类的方式比直接用HTTP状态码传达更多信息前端拿到code之后可以精确地给出对应的用户提示。分页接口统一接收page和page_size参数返回格式包含总数和列表数据{ code: 0, message: ok, data: { list: [], total: 50, page: 1, page_size: 10 } }总数字段必须由后端返回不能前端自己算原因是当数据量变大之后前端一次性拉取所有数据做本地分页的方案会快速失效后端分页是唯一可行的路径。因此从第一版开始就养成这个好习惯后续数据量上来也不会踩坑。3. 后端实现从零搭建Gin服务3.1 项目初始化与依赖管理后端项目我用go mod管理依赖。模块名取的是task-manager-server后续所有内部包都基于这个路径来引用。初始化命令和依赖安装如下go mod init task-manager-server go get -u github.com/gin-gonic/gin go get -u gorm.io/gorm go get -u gorm.io/driver/mysql go get -u github.com/golang-jwt/jwt/v5 go get -u golang.org/x/crypto/bcrypt go get -u github.com/gin-contrib/cors配置管理我选择了最朴素的方式——一个config.yaml文件加一个结构体。数据库地址、端口、JWT密钥、token过期时间都放在这里。配置从环境变量读取的方式更适合生产环境但个人项目用yaml文件已经足够了关键是把JWT密钥单独留成环境变量注入不要把生产环境的密钥提交到代码仓库。连数据库的代码要注意GORM的时区设置。直接在DSN参数里加上locLocal和parseTimeTrue前者保证时间按本地时区存取后者让MySQL的DATETIME类型能自动映射到Go的time.Time类型。不加这两个参数查询出来的时间字段会出现8小时的时差排查起来非常头疼。3.2 JWT认证与用户模块用户模块的核心是注册和登录。注册时密码不能明文存储我用的是bcrypt算法做哈希。相比MD5和SHA家族的快速哈希算法bcrypt是专门为密码存储设计的慢哈希自带盐值同样的密码每次生成的哈希值都不同暴力破解成本高得多。虽然bcrypt计算稍慢但认证场景下这个开销完全可以接受。登录成功后签发JWT载荷只放三个核心字段用户ID、用户名、过期时间。JWT的内部信息不适合放太多东西因为token的体积会影响请求头的大小同时也会增加token泄露后的风险面。关键信息需要查询时再从数据库读取而不是一股脑塞进token里。claims : jwt.MapClaims{ user_id: user.ID, username: user.Username, exp: time.Now().Add(24 * time.Hour).Unix(), }认证中间件的逻辑值得一提。JWT的验证不是在每个接口里重复写的而是通过Gin中间件统一拦截。在routes注册时需要登录的接口都加在这个中间件之后。中间件解析token成功后把用户信息塞进请求上下文后续业务代码直接c.Get(user_id)就能拿到当前用户ID不用再去查询数据库。3.3 任务模块核心接口实现任务模块的接口是标准REST风格GET列表、POST创建、PUT更新、DELETE删除、PATCH改状态。每个接口第一件事是确认资源归属权——从JWT里拿到的userID和待操作任务的user_id比对不一致直接返回无权访问。任务管理器是私有的个人工具不存在共享场景所以这个校验非常简单。创建任务的逻辑包含参数校验和默认值处理。标题必填、长度控制在200字以内状态默认待办、优先级默认中截止时间可以留空。校验逻辑放在service层而不是controller层是为了保证复用性。如果后面做批量导入功能同样的检查逻辑可以直接调用service层方法。列表查询是接口里最有讲究的一个。它需要组合筛选、搜索、分页、排序多个维度。GORM的链式调用写起来很清晰query : db.Where(user_id ?, userID) if req.Status ! nil { query query.Where(status ?, *req.Status) } if req.Priority ! nil { query query.Where(priority ?, *req.Priority) } if req.Keyword ! { query query.Where(title LIKE ?, %req.Keyword%) }排序逻辑做了白名单控制。前端传入的sort_by字段只会被映射到created_at、due_date、priority三个固定字段这样可以避免数据库列名被外部任意指定。有些项目会在排序字段上直接拼接SQL造成注入漏洞白名单映射是最稳妥的做法。3.4 中间件CORS、日志与错误恢复CORS中间件是前后端分离项目必须处理的环节。开发环境前端跑在5173端口后端跑在8080端口跨域请求如果不在后端允许跨域浏览器会直接拦截。我用的gin-contrib/cors配置了允许的来源列表、允许的方法和请求头。生产环境如果由Nginx反向代理前后端变成同源这个中间件就不起作用了但保留着没有坏处以后前后端域名分离也不用再改后端。日志中间件也是必需的。每个请求的IP、路径、耗时、状态码会输出到终端和文件。开发时定位问题主要靠它——某个接口请求超时了看日志就知道耗时多少某个接口返回500了错误栈在哪里也能从日志里翻到。Gin框架自带的Recovery中间件可以拦截运行时panic防止整个服务因一个协程的崩溃而挂掉。我的处理方式是在Recovery之外再加一层自己的错误处理逻辑业务错误通过c.AbortWithStatusJSON直接返回统一的错误响应同时用logger记录错误的详细信息方便调试。4. 前端实现Vue3 Pinia Element Plus4.1 前端工程化与目录规划前端我用Vite搭建工程它的冷启动速度和热更新体验明显比Webpack好新项目直接用它不会有任何纠结。脚手架创建完之后的目录规划很关键我按下面的结构整理task-manager-web/ ├── src/ │ ├── api/ │ │ ├── auth.ts │ │ └── task.ts │ ├── assets/ │ ├── components/ │ │ ├── TaskFormDialog.vue │ │ └── TaskItem.vue │ ├── router/ │ │ └── index.ts │ ├── stores/ │ │ ├── auth.ts │ │ └── task.ts │ ├── views/ │ │ ├── LoginView.vue │ │ └── TaskManagerView.vue │ ├── types/ │ │ └── index.ts │ ├── utils/ │ │ └── request.ts │ └── App.vueTS类型定义是整个前端开发里我最看重的一部分。后端每个接口的请求参数和响应结构在前端都定义成对应的interfaceAxios二次封装的Request函数是泛型化的接口调用时天然具备完整的类型提示。找人协作开发时只看类型定义就能知道接口的输入输出长什么样根本不用打开后端代码。4.2 登录与状态持久化登录页做得简洁核心逻辑在auth store里。用户输入用户名密码前端调登录接口成功后把token存入内存和localStorage同时调用获取用户信息接口把用户信息写入store。之后每次发请求Axios的请求拦截器都会从store取token并添加到Authorization头。响应拦截器里做了统一的错误处理。业务错误返回的code不为0时根据错误码给出不同的提示信息比如10001就是登录过期直接弹出提示并跳转到登录页。网络错误单独处理提示用户检查网络连接。这套机制写完后每个具体页面的接口调用代码都非常干净不用重复处理错误分支。路由守卫控制页面访问权限。任务管理页面要求必须有token才能进没有token访问时自动重定向到登录页。这个跳转逻辑容易在细节上出问题——访问被守卫拦截跳转到登录页时next函数要用next(/login)而不是直接next(false)并且要把用户原本想去的完整地址记录下来登录成功后路由到目标页面而不是固定首页。4.3 任务列表筛选、排序与分页任务列表是全项目前端工作量最大的模块。顶部是一排筛选条件包括状态、优先级、搜索关键词中间是表格区域展示标题、状态标签、优先级标签、截止时间、创建时间右侧是编辑和删除按钮底部是分页组件。筛选条件变化时重新拉列表数据这个交互看起来很自然实现上有几个细节需要考虑。筛选状态我用URL参数和store状态双重管理。用vue-router的query参数同步筛选条件好处是刷新页面、分享链接时筛选状态不会丢同时把当前筛选条件放进store切换页面后回来还能保持住。两者同步的逻辑写起来稍微绕一点但在任务管理这种频繁切换场景里体验提升相当明显。状态和优先级在表格里用el-tag组件展示根据数值映射不同的文案和颜色。我把映射逻辑抽成一个公共函数比如状态0显示灰色“待办”标签、状态1显示蓝色“进行中”、状态2显示绿色“已完成”优先级1、2、3分别对应“低”、“中”、“高”颜色从灰色渐变到红色。这样数值和展示完全解耦后端传什么值前端只做翻译遇到异常值还能从视觉效果上一眼发现。4.4 移动端适配与uniapp复用策略一开始我也试图用uniapp写一套移动端代码把Web端的功能完整复制一份。做到一半我发现问题很大两套代码维护成本翻倍、移动端交互和Web端差异大、细节打磨时间不够。冷静下来之后我调整了策略——第一个版本用响应式布局让Web端适配移动浏览器uniapp的完整App版本放到后置计划。响应式方案上Element Plus的表单组件自带栅格系统表单在窄屏时自动变成单列表格在移动端体验很差所以我额外加了一个移动端友好的卡片视图。通过CSS媒体查询判断屏幕宽度窄屏时表格隐藏、卡片显示每条任务以卡片形式展示在屏幕上操作按钮改成图标加文字。大部分工具类应用在手机浏览器里的可用性已经很高了如果后续用户反馈强烈再开发真正的移动端App。真正从系统层面做跨端规划的核心是API的复用。这套架构的价值在于无论是uniapp还是小程序后续只需要重新写一层UI和API调用后端完全不需要改动。因为所有的业务逻辑都封装在服务端客户端的职责被压缩到最小。5. 部署、常见问题与优化方向5.1 本地联调与生产部署方案本地开发时前端通过Vite的代理配置解决跨域问题生产环境的前后端统一由Nginx承载。Nginx配置里有几个关键点值得记录server { listen 80; server_name your_domain.com; # 前端静态文件 root /var/www/task-manager; index index.html; # 前端路由history模式刷新404问题靠这个解决 location / { try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }后端编译后直接扔到服务器上运行注意Gin默认是debug模式会打印大量调试日志。生产环境用环境变量切换到release模式GIN_MODErelease ./task-manager-server。数据库连接、JWT密钥这些配置通过config.yaml读取JWT密钥单独留给环境变量注入。5.2 常见问题速查表搭建和联调过程中踩过的坑我把它整理成了速查表遇到同类问题的朋友可以直接对照排查问题现象根因分析解决方案前端请求接口报CORS错误后端未配置跨域白名单在CORS中间件中加入前端请求来源域名时间字段差了8个小时数据库连接DSN缺少时区参数DSN追加locLocalparseTimeTrue刷新页面404Vite开发环境路由未配置history回退服务端配置try_files回退到index.html修改任务变成了新增更新时缺少ID传递或误用create方法更新操作显式调用db.Model(task).Updates()token过期后接口返回不统一中间件未捕获JWT的校验错误中间件统一处理token解析的各类错误返回登录状态刷新丢失token未持久化或store恢复逻辑缺失应用启动时从localStorage读取token并校验有效数据库时间时区这个坑我印象最深。有一次测试新增任务前端显示的截止时间明明是晚上8点保存后查数据库变成了凌晨0点差了整整8个小时。一开始以为是前端传参问题后来逐层排查才发现是数据库连接的DSN没有指定locLocalGo的database/sql默认把DATETIME按UTC解析了。改掉之后时间就正常了。5.3 项目复盘与后续扩展计划项目做完之后我重新审视了一遍代码觉得有两个地方当时可以做得更好。一是状态流转虽然加了后端检查但检查逻辑只是简单的条件判断没有抽成独立的状态机模块。如果后续要增加“已取消”、“已推迟”之类的状态现在的结构会越改越复杂。二是前端对删除操作没有做过软删除——用户误删任务后数据直接物理不存在了没有任何后悔的机会。从用户体验上讲加一个回收站机制会更友好。后续我给自己列了一份清晰的扩展清单按优先级排列第一是任务标签的多选支持现在只是一个字符串字段改成关联表后才方便按标签集中筛选第二是数据看板用图表展示一周内任务的完成情况和逾期情况第三是日历视图把截止日期映射到月历上这是任务管理工具使用频率最高的功能之一第四是微信小程序版本API已经准备好了前端复用现有逻辑改造即可。做完这个项目最大的感受是全栈开发真正的难点不在某个具体技术而在于思维的频繁切换。写后端时你考虑的是数据怎么存、规则怎么定、边界怎么处理切到前端你要考虑的是用户想要什么反馈、交互是否直觉、异常状态怎么呈现。这种频繁的身份切换会逼着你建立一套完整的闭环思维而这恰恰是独立开发一个产品最宝贵的能力。如果有朋友看完这篇总结正准备动手我的建议是先别着急写代码花几天时间把需求、表结构、接口定义画清楚这个前期时间花得越充分后期返工的次数就越少。
