从零到一打造企业级全栈后台管理系统 —— 技术选型、工程化实践与深度思考

耗时数月独立开发,摘录最具代表性的架构决策与实现细节,欢迎 Star 交流。


一、项目定位:前端开发者进阶全栈的桥梁

YunHe-Vue(云禾管理系统)是一套面向前端开发者进阶全栈的企业级后台管理系统模板。

它的核心设计理念是:TypeScript 一把梭。前端用 Vue 3 + TS,后端用 NestJS + TS,同一门语言、同一套类型系统,配合 NestJS 与 Vue 高度相似的「模块化 + 依赖注入 + 装饰器」开发范式,让前端开发者在熟悉的生态里完成全栈能力的平滑过渡。

项目信息 地址
在线演示 cnbox.online
项目文档 ace627.github.io
开源仓库 github.com/Ace627/YunH...
文档仓库 github.com/Ace627/ace6...

⚡ 项目正在持续迭代维护中,Star 是最好的更新动力。


二、技术全景

层级 技术 选型理由
前端框架 Vue 3.5 + Composition API 最新 Vue 生态,<script setup lang="ts"> 语法
前端路由 Vue Router 5 动态路由 + 权限过滤,路由配置完全由后端下发
状态管理 Pinia 3 轻量、类型友好,天然适配 Composition API
UI 组件库 Element Plus 2 企业级组件库,按需导入,SCSS 变量主题化
构建工具 Vite 8 + Rolldown 秒级冷启动,内置拆包 / 压缩 / 分析全链路
CSS 方案 SCSS + BEM + UnoCSS 语义化主方案 + 原子化为辅,灵活可切换
后端框架 NestJS 11 企业级 Node.js 框架,模块化 + 依赖注入 + 装饰器
ORM TypeORM Active Record / Data Mapper 双模式,支持 Migration
数据库 MySQL 8 + Redis 7 业务数据 + 缓存 / 队列
消息队列 BullMQ 基于 Redis 的可靠队列,支撑定时任务调度
AI 服务 LangChain + OpenAI 流式对话、上下文管理、Token 统计
容器化 Docker + Nginx 多阶段构建 + 健康探针 + Nginx 反向代理 + gzip_static
Monorepo pnpm workspace 前后端同仓,统一脚本,共享类型

三、核心系统设计 ------ 每一个功能都有它的设计考量

1. 前后端一体化的 RBAC 权限模型

权限模型是后台系统的骨架,设计不好会让整个项目越写越乱。我的方案是 RBAC(基于角色的访问控制)+ 前后端双闭环

后端侧

  • 用户 → 角色 → 菜单,菜单粒度为「目录 / 菜单 / 按钮」三级
  • 每个接口通过 @RequirePermissions 装饰器声明所需权限码
  • JWT 签发时写入用户角色与权限集合,Guard 层统一校验
  • 公共接口通过 @Public 装饰器标记跳过鉴权

前端侧

  • 菜单数据由后端下发,前端递归生成路由表并动态注册
  • 路由守卫在页面跳转时校验权限,无权限重定向 403
  • v-permissions 指令控制按钮级 DOM 显隐,比 v-if 更语义化
  • v-roles 指令按角色控制,适用场景比权限码更粗粒度
  • Permission Store 集中管理路由状态,全局可消费

设计价值:权限配置全部收敛到后台管理界面,增删菜单角色无需动代码、无需重新部署前端,真正实现运维与开发分离。


2. 大文件分片上传 ------ 完整的企业级文件传输方案

处理大文件上传不是「能传上去就行」,真正的挑战在于:上传中断后怎么办?重复上传同一文件怎么避免?合并分片时怎么不把服务器内存打满?

我的方案覆盖了全生命周期:

前端:计算文件 MD5 → 调用检查接口 → 已存在直接秒传,未上传则按分片并发上传 → 全部上传完成调用合并接口。

后端checkFile 接口先查目标文件是否存在(秒传),再查临时目录有哪些分片(断点续传),返回已上传的分片列表,前端只传缺失分片。合并时使用 Node.js Stream 管道,边读边写不占内存 ,这在文件较大的情况下与 fs.writeFileSync(buffer) 是数量级的差距。

typescript 复制代码
// 流式合并------不积攒内存,一台小机器也能合大文件
const writeStream = createWriteStream(finalFilePath)
for (const chunk of chunks) {
  await pipeline(createReadStream(resolve(tempDir, chunk)), writeStream, { end: false })
}
writeStream.end()

📌 当前因服务器资源有限,文件写入本地 uploads 目录。但合并逻辑基于 Stream 抽象,替换读写目标为 MinIO / OSS 的 SDK 流即可无缝接入云存储,按需改造即可,不涉及业务层改动。


3. 动态定时任务调度 ------ 从硬编码到可视化

传统定时任务最常见的痛点是:需求让你凌晨 3 点跑个 Excel 导出,你得改代码发版。

我的方案基于 BullMQ + Redis 实现运行时动态调度:

  • 所有业务 Service 通过 NestJS 的 DiscoveryService 自动扫描注册到任务服务中
  • 新增任务只需在后台界面填写「Service 类名.方法名(参数)」即可,格式校验由 analysisInvokeTarget 方法完整覆盖
  • Cron 表达式的启停、修改、手动执行全部通过 API 操作,无需重启服务
  • 任务执行失败时,支持「立即重试」与「执行一次」两种错误策略,杜绝雪崩
  • 完整的日志记录、分页查询、Excel 导出
typescript 复制代码
// 自动发现业务 Service,无需手动注册
private loadBusinessServices() {
  const providers = this.discovery.getProviders()
  for (const wrapper of providers) {
    const { metatype } = wrapper
    if (!metatype || !metatype.name.endsWith('Service')) continue
    this.serviceMap.set(metatype.name, metatype)
  }
}

设计价值:任务配置全部可视化,运维人员不需要理解代码,开发者不需要改代码发版。


4. AI 对话模块 ------ 企业级落地 LangChain 的完整实践

AI 功能很容易做成 Demo,但真正上线需要考虑:Token 成本怎么控制?长对话上下文怎么管理?并发请求怎么处理?

我的落地策略:

  • 流式响应:使用 SSE 协议逐字返回,用户体验类似 ChatGPT
  • 懒更新摘要 :不是每轮对话都调 AI 生成摘要。设定阈值(如 12 条消息),且只有消息数到达阈值后才每隔 6 条消息生成一次摘要,大幅削减 AI 调用次数
  • 上下文策略:摘要 + 最近 6 条消息组装上下文,在上下文完整度与 Token 消耗之间取平衡
  • 历史持久化:消息、会话、摘要、Token 用量全部入库,支持会话列表与管理
typescript 复制代码
// 懒更新摘要------只在需要的时候才调 AI,用最小的成本保留上下文
private async lazyUpdateSummary(conversationId: string) {
  const count = await this.messageRepository
    .createQueryBuilder('message')
    .where('message.conversationId = :conversationId', { conversationId })
    .getCount()

  if (count < 12) return                              // 不够 12 条,不生成
  if ((count - 12) % 6 !== 0) return                   // 不在更新节点,跳过

  const summary = await this.generateSummary(conversationId)
  await this.conversationRepository.update(conversationId, { summary })
}

设计价值:这不是一个玩具 Demo,而是真正考虑了生产环境成本和可维护性的工程方案。


5. 云原生健康检查 + 企业级日志 + 操作审计

这一组模块共同构成了系统的「可观测性」:

模块 实现
健康检查 /live(存活探测)+ /ready(就绪探测)双探针,适配 K8s Pod 生命周期;网络 / 数据库 / 内存 / 磁盘 / RSS 全维度检查
日志系统 Winston 集成,按日期滚动分割,压缩归档,自动清理 14 天前日志;开发环境控制台输出,生产环境 JSON 文件
操作日志 OperationLogInterceptor 拦截器,自动记录请求耗时、IP + 归属地、query + body 参数、成功/失败状态
登录日志 登录成功 / 失败全量记录,IP + 归属地 + 设备 + 浏览器信息
在线用户 Redis 维护活跃 Session,支持强制下线

四、前端工程化 ------ 让代码自己说话

如果说后端系统设计体现的是架构能力,那前端工程化体现的是开发者对开发体验和代码品质的追求

4.1 构建全链路优化

智能拆包

不是简单地把 node_modules 拆成 vendor 就完了。我按模块体积、更新频率、缓存策略做了精细化分组:

typescript 复制代码
// vite.config.ts
codeSplitting: {
  groups: [
    { name: 'element-plus', test: /node_modules[\\/]element-plus/, priority: 30 },
    { name: 'echarts', test: /node_modules[\\/](echarts|zrender)/, priority: 25 },
    { name: 'vue-vendor', test: /node_modules[\\/](vue|vue-router|pinia)/, priority: 15 },
    { name: 'utils', test: /node_modules[\\/](dayjs|axios|lodash-es)/, priority: 12 },
    { name: 'vendor', test: /node_modules/, priority: 10 },
  ]
}

设计逻辑element-plusecharts 体积大且更新频率低,最高优先级独立拆包,最大化浏览器缓存命中率;vue-vendor 是框架本身,几乎不更新,也单独拆出;剩余的按优先级逐级兜底。

压缩与部署闭环

  • 构建端vite-plugin-compression 生成 .gz 文件,压缩级别 9,阈值 10KB
  • 部署端 :Nginx 开启 gzip_static on,直接使用构建产物,不在服务器端实时压缩,降低 CPU 负载
  • 环境变量VITE_DROP_CONSOLEVITE_DROP_DEBUGGER 控制产物清理,生产环境自动剔除

打包分析

集成 rollup-plugin-visualizer,构建后自动生成 stats.html,展示各模块体积和 gzip / brotli 压缩对比,方便做针对性优化。


4.2 开发体验优化

自动按需导入

三个插件形成闭环:

typescript 复制代码
// vite.config.ts
// 1. Element Plus 组件 + 样式按需导入
ElementPlus({ useSource: true })

// 2. Vue / Pinia / Vue Router / VueUse 自动导入
AutoImport({
  resolvers: [ElementPlusResolver({ importStyle: 'sass' })],
  imports: ['vue', 'pinia', 'vue-router', '@vueuse/core'],
  dts: 'types/auto-generate/auto-import.d.ts',
  dirs: ['src/store/modules', 'src/hooks'],
})

// 3. 组件自动注册
AutoComponents({
  resolvers: [ElementPlusResolver({ importStyle: 'sass' })],
  dts: 'types/auto-generate/auto-components.d.ts',
})

效果refcomputedonMounted、Store、Hooks 全部告别手动 import,IDE 类型提示完美运行。

UnoCSS 原子化 CSS

基于 presetWind3 预设,兼容 Tailwind / Windi / Bootstrap 规则,同时封装了项目级快捷方式和自定义规则。作为 SCSS + BEM 主方案的补充,在需要快速布局或微调间距的场景下使用,不强制,不捆绑。

SVG 图标管理自动化

给设计师要的 SVG 经常带一堆 AI / Sketch 导出的冗余属性。写了一个脚本:

json 复制代码
// package.json
"svg:clean": "esno ./scripts/svg-clean.ts"

丢到 src/assets/icons 目录下,执行 pnpm svg:clean,批量清理无用属性。同时所有图标统一通过 SvgIcon 全局组件使用,name 直接取文件名,零配置。


4.3 组件设计的取舍

ProTable / ProSearch / ProPagination:组合而非集成

很多后台模板喜欢做一个巨型 ProTable,把搜索、表格、分页全部塞到一起,用起来确实方便,但一旦有定制需求就无从下手。

我的方案是三个独立组件,各司其职

  • ProTable 负责表格渲染,通过 Proxy 代理暴露所有 Element Plus 原生方法
  • ProSearch 负责搜索表单,配置化生成
  • ProPagination 负责分页逻辑

最大的好处是:你不需要的时候可以不引入,需要定制的时候可以直接用 Element Plus 原生组件,不会被封装层束缚。

ECharts 封装:按需引入 + 防御式设计

ECharts 全量引入会显著增加包体积,我用了一个专门的入口文件只注册项目实际使用的图表类型和组件。ProChart 组件内部用 ResizeObserver 监听容器变化自动重绘,主题切换时同步更新配色,onUnmounted 中调用 dispose 防止内存泄漏。

DictTag:字典回显的最后一公里

前面通过 useDict Hook 将字典数据全局缓存到了 Pinia Store,但回显时总不能每处都手写 dictList.find()DictTag 组件封装了这一步------传入字典类型和值,自动匹配回显文案和样式:

vue 复制代码
<DictTag dictType="sys_status" :value="row.status" />

4.4 一个你可能没注意到的细节:KeepAlive 自动匹配

动态加载的组件没有静态 name,KeepAlive 无法匹配,缓存失效。传统做法是手动写 defineOptions({ name: 'xxx' }),但动态路由上百个页面,漏一个就失效。

我的方案是在组件加载时自动注入 name

typescript 复制代码
// router.helper.ts
export function loadView(componentPath: string) {
  // ...
  const componentName = upperFirst(camelCase(componentPath.replace('index', '')))
  const component = views[path]
  return () => component().then((comp) => ((comp.default.name = componentName), comp))
}

动态页面一个 defineOptions 都不用写,只有静态路由(如 404)才需要。这个细节能体现你对 Vue 组件加载与 KeepAlive 匹配机制的深入理解。


4.5 更多工程化细节速览

模块 做了什么
缓存工具 参考 Redis 接口封装 localStorage,支持设置过期时间、通配符 key 查询、获取剩余 TTL,统一缓存键常量管理,避免硬编码和冲突
进度条 useProgress Hook 封装 nprogress,支持从环境变量控制是否开启,默认配置合并,在 axios 拦截器和路由守卫中统一触发
动态标题 useDynamicTitle Hook,监听路由变化自动更新 document.titleonUnmounted 清理监听器
递归菜单 SidebarItem 组件递归渲染任意层级菜单,单叶子节点自动提升,外部链接新窗口打开,CSS 变量主题化
系统配置 defaultSettings 集中管理所有配置项默认值,SettingPanel 可视化配置面板,用户偏好自动持久化到 local,支持一键重置
首屏动画 纯 CSS 实现加载动画,避免白屏
主题切换 View Transition API 实现从点击位置向外扩散的圆形过渡动效,SCSS @forward 自定义 ElementPlus 主题变量,Vite 预处理器自动注入
模块化启动 main.ts 只负责串联,setupPlugins / setupDirectives / setupStore / setupRouter 各自独立,职责清晰

五、后端架构 ------ 分层解耦与防御性设计

5.1 响应缓存拦截器 + 防缓存雪崩

热点接口频繁调用会打满数据库连接。我在 NestJS 拦截器层面实现了一套缓存方案:

  • @ResponseCache 装饰器声明缓存 key 和 TTL
  • ResponseCacheInterceptor 透明拦截:命中 Redis 直接返回,未命中执行接口后写入 Redis
  • TTL 加随机抖动:每个 key 的过期时间在上限基础上叠加 0~20% 的随机偏移,避免缓存同时过期引发雪崩
typescript 复制代码
// 防雪崩的 TTL 计算
const baseTtl = options.ttl || 60
const jitter = Math.floor(Math.random() * (baseTtl * 0.2))
const ttl = baseTtl + jitter

这与 Redis 官方的 EXPIRE 抖动策略原理一致,但你不需要在业务代码里重复实现,一个装饰器搞定。


5.2 异常处理

AllExceptionsFilter 全局捕获异常,区分 BusinessException(业务异常,返回友好提示)与未知异常(记录日志并返回通用错误信息),避免敏感信息泄露到前端。


5.3 安全防护

  • Helmet:设置安全 HTTP 头
  • 限流ThrottlerGuard 限制接口调用频率
  • 密码加密:Argon2 算法,比 bcrypt 更抗 GPU 暴力破解

六、快速开始

项目脚本一览

json 复制代码
{
  "scripts": {
    "dev:server": "pnpm --filter server start:dev",
    "build:server": "pnpm --filter server build",
    "dev:admin": "pnpm --filter admin dev",
    "build:admin": "pnpm --filter admin build",
    "docker:up": "docker compose up -d --build",
    "docker:down": "docker compose down",
    "docker:restart": "docker compose down && docker compose up -d --build",
    "docker:reset": "docker compose down -v && docker compose up -d --build",
    "svg:clean": "esno ./scripts/svg-clean.ts"
  }
}

Docker 一键启动(推荐)

bash 复制代码
pnpm docker:up

服务启动后访问 http://localhost 即可。

本地开发

bash 复制代码
pnpm install
pnpm dev:admin   # 前端
pnpm dev:server  # 后端

七、写在最后

这个项目不是抄模板拼凑出来的,是我一个人从空目录开始,一步步搭建架构、封装组件、解决 Bug、打磨细节的结果。

它代表了我对「什么是高质量代码」的理解:

  • 组件封装要有边界感------不把功能做死,不把灵活度做没
  • 工程化配置要形成闭环------构建、压缩、部署三端对齐,不搞半吊子方案
  • 每个功能都要经得起追问------不只是「能跑」,而是能说出「为什么这样设计」

如果你也在进阶全栈的路上,希望这个项目能给你一些参考和启发。遇到任何问题,欢迎提 Issue 交流。

⚡ 项目正在持续迭代维护中,觉得有帮助的话,Star 是最好的鼓励。


开源仓库github.com/Ace627/YunH...

在线演示cnbox.online

项目文档ace627.github.io

相关推荐
竹林8181 小时前
用 ethers.js 连 MetaMask 做钱包登录,我踩了三个坑才搞定跨页面状态同步
前端·javascript
饺子不吃醋1 小时前
深入理解 Vue 3 的 setup(含 Composition API)
前端·vue.js
阿星做前端1 小时前
重度 AI 编程用户的一天:我怎么把 Claude Code / Codex 工作流搬进浏览器工作台
前端·javascript·后端
风止何安啊1 小时前
手写 URL 解析器,面试官到底想考什么?
前端·javascript·面试
yingyima2 小时前
踩坑亲历:一次因 JSON 格式问题导致的宕机,及工具救赎
前端
kyriewen2 小时前
我开发的 Chrome 扒图浏览器插件又更新了❗
前端·chrome·浏览器
程序员祥云2 小时前
Prompt项目说明文档
前端
一勺菠萝丶2 小时前
如何在 Linux 服务器上使用 Speedtest 官方 CLI 测试带宽(小白教程)
java·服务器·前端
DianSan_ERP2 小时前
京东订单接口集成中如何处理消费者敏感信息的安全与合规问题?
前端·数据库·后端·团队开发·运维开发