ever-gauzy 开源业务管理平台:Nx Monorepo 多租户部署实践 做企业内部系统的朋友大概都遇到过这样的场景时间跟踪一个工具、发票一个工具、CRM 再开一个、HR 又得单独买一套最后每个月在几套系统之间来回切账号数据还互不相通。我最早接触ever-gauzy就是因为这个痛点——它是一个把时间跟踪、发票管理、CRM、HR、项目管理塞进同一套代码库的开源业务管理平台代码托管在 GitHub 的 ever-co 组织下采用 Nx monorepo 组织后端 NestJS TypeORM前端 Angular桌面端 Electron移动端 React Native。换句话说它想做的事情是你自建一套业务数据全部留在自己手里字段想加就加报表想改就改。我前后在测试环境和一台小型生产机上跑过几个版本踩过的坑不算少也不算什么专家就把这一路的过程和一些关键决策点整理出来。这篇文章适合三类人正在给团队找轻量自建管理系统的技术负责人、想拿一个真实的大型 monorepo 项目练手的开发者、以及已经部署了 ever-gauzy 但卡在某个环节想找排查思路的人。下面我按“整体思路、技术选型、跑起来的完整流程、集成扩展、问题排查、部署经验”的顺序讲尽量把每一步为什么这么做也说清楚。1. ever-gauzy 的定位与整体架构思路拆解1.1 一个平台覆盖哪些业务域先把边界说清楚不然很容易上来就被目录结构劝退。ever-gauzy 的核心业务模块大致有这么几块我用一张表对照说明方便你判断它是否契合自己的需求。业务域主要实体典型用途时间跟踪TimeLog、Timesheet、Activity按任务/项目记录工时桌面端采集活动与截图发票与账单Invoice、InvoiceItem、Estimate、Payment按工时生成发票、多币种、导出 PDF项目管理OrganizationProject、Task、Sprint任务看板、团队分配、进度统计CRMContact、OrganizationContact客户线索、联系人、合作方管理HR 与团队Employee、OrganizationTeam、Expense员工档案、团队编制、报销与设备集成Integration、IntegrationTenant与外部平台做数据同步这里有个设计取舍值得说ever-gauzy 把“组织Organization”和“租户Tenant”拆成了两层。一个 Tenant 可以拥有多个 Organization用户通过角色和权限Role、Permission挂到具体组织上。为什么不在一个组织里打平因为实际业务里很多团队是一个人同时服务于几家关联公司或者一家集团下面有多个独立核算的子公司。如果强行用一个组织维度权限和报表全要重塑。多租户加多组织的双层结构代价是查询时几乎每张表都要带tenantId和organizationId过滤好处是后续做数据隔离和按组织出报表时不用推倒重来。我在实际配置时就吃过一次亏。刚开始为了图省事把所有项目和员工都挂在默认组织下等到要按业务线拆分成本时发现历史数据已经混在一起只能手工迁移。所以如果你打算长期用前期就把组织划分想清楚哪怕现在只有一个组织也建议按“未来可能独立核算”的粒度来建。1.2 Nx monorepo 的目录结构与包边界ever-gauzy 整个仓库是一个 Nx 工作区这是它最容易被新手忽略、但最影响开发体验的一点。简单说Nx 把“多个可独立构建的应用”和“多个可复用的库”放在同一个仓库里靠依赖图来决定构建顺序和缓存。它的目录大致是这样组织的ever-gauzy/ ├── apps/ # 可独立运行的应用 │ ├── api/ # NestJS 后端服务 │ ├── gauzy/ # Angular 主前端 │ ├── desktop/ # Electron 桌面端 │ ├── desktop-timer/ # 桌面计时器 │ └── mobile/ # React Native 移动端 ├── packages/ # 可复用的库与共享代码 │ ├── core/ # 后端核心业务逻辑、实体、DTO │ ├── contracts/ # 前后端共享接口定义与枚举 │ ├── ui/ # 前端 UI 组件库 │ └── ... └── nx.json / package.json # 工作区配置与脚本入口为什么值得单独讲因为很多人上手第一反应是cd apps/api npm install然后发现依赖装不全、命令跑不起来。Nx 工作区的依赖统一在根目录安装所有nx serve、nx build命令也都在根目录执行。packages/contracts这个包尤其要注意它同时被前后端引用改一个枚举可能影响两端这是 monorepo 的典型收益也是典型风险——你改的是一个共享类型编译期就能发现问题但也会同时牵动多个应用。基于常见实践我建议在本地开发时养成分包构建的习惯先确认依赖图yarn nx graph打开可视化页面看看你要改的库被哪些应用依赖避免改完自己那一块结果别的应用编译不过。1.3 多端复用与前后端分离的边界ever-gauzy 的多端不是三套独立代码而是尽量复用packages/contracts里的类型定义和packages/core里的业务规则。Web 端和桌面端共用 Angular 生态移动端走 React Native各自打包壳不同但数据模型一致。这种安排的好处很直观工时记录的逻辑只写一次桌面端和 Web 端的行为保持一致。代价是构建链路变长尤其桌面端要打 Electron 包的时候首次构建比较磨人。我在一台 8 核 16G 的机器上跑完整构建首次大概要十几分钟后续有 Nx 缓存会快很多。所以如果你只是想先看看界面别急着构建桌面端先把 API 和 Web 端跑起来就够体验了。2. 核心技术栈选型与背后的取舍2.1 NestJS 加 TypeORM 这套组合的真实意图后端选 NestJS 而不是裸 Express我认为核心原因有三个理解了这三点你在读代码时就不会觉得它“绕”。第一是模块化。NestJS 的 Module 机制天然适合同一个仓库里塞进十几个业务域每个域自己管自己的 Controller、Service、EntityModule声明依赖谁依赖谁一眼能看出来。ever-gauzy 的packages/core里就是这么组织的时间跟踪一个模块发票一个模块集成一个模块边界相对清晰。第二是依赖注入带来的可替换性。比如邮件发送、文件存储、集成同步这些能力都通过 Provider 注入想换成自建对象存储或换一家 SMTP 服务改动面比较可控。第三是 TypeORM 的迁移与实体能力。业务系统最怕的就是表结构变更没留痕TypeORM 的 migration 机制要求你把每次结构变更写成可回滚的脚本这在多租户系统里尤其重要因为一张表加字段可能同时影响所有租户。提示TypeORM 有个容易踩的点是synchronize。开发时把它打开很省事实体会自动同步到数据库但生产环境一定要关掉否则一次误改实体就可能把线上表结构改坏。用 migration 才是稳妥路径。2.2 Angular 与 RxJS 的投入值不值前端选 Angular说实话对习惯了 React/Vue 的人不算友好尤其它大量使用 RxJS 的 Observable 和操作符。我第一次读packages/ui里的服务时看到switchMap、takeUntil、combineLatest混在一起确实头大。但从长期维护角度看这个选择有它的道理。企业管理系统的表单极其复杂一个发票页面上几十个字段、动态行、多币种、联动校验Angular 的响应式表单Reactive Forms在这种场景下比手写受控组件要规整得多。再加上依赖注入和强类型大型团队并行开发时冲突会少一些。我个人的经验是如果你只是要部署不用花太多精力啃前端源码但如果你打算深度定制界面建议先花两天把 RxJS 的订阅取消机制搞明白。前端常见的内存泄漏和“接口请求莫名触发两次”八成都是订阅没清理干净或者操作符用错导致的。2.3 数据库与多租户策略的选择ever-gauzy 默认推荐 PostgreSQL同时也能跑在 SQLite 上。这个差别不是小事我用一张表说明什么时候选哪个。维度PostgreSQLSQLite并发写入支持高并发适合多人同时打卡、记工时单写入锁多用户会排队部署成本需要一个独立服务占用内存较高一个文件搞定零运维多租户能力可以配合 schema、索引、行级策略做隔离只能靠应用层 tenantId 过滤适用场景团队规模 10 人以上、正式使用单机演示、个人试用、离线评估需要强调的是ever-gauzy 的多租户本质上是“共享表 应用层过滤”也就是所有租户的数据存在同一批表里靠tenantId字段区分。这种做法的好处是维护简单、迁移方便坏处是任何一处查询漏加租户条件就可能出现跨租户数据可见。这也是我在生产环境里最警惕的地方。注意如果你在自定义查询或写原生 SQL务必手动带上租户过滤条件。别指望框架帮你兜底尤其是自己新加的报表接口。3. 本地从零跑起来的完整实操流程3.1 环境准备与版本对齐这一节是纯实操我按自己的操作顺序写。先看环境要求版本不对齐是最常见的失败原因。# 建议使用 Node 18 或 20 的 LTS 版本避免使用过新的奇偶版本 node -v # 建议 Yarn 1.x仓库使用 yarn 作为包管理器 yarn -v # PostgreSQL 如果你打算用本地库 psql --version为什么强调版本因为这类大型 monorepo 依赖树很深某些原生模块比如数据库驱动、图像处理相关依赖在 Node 版本跨大版本时经常编译失败。我的做法是用版本管理工具锁定一个 Node LTS别用系统自带的版本也别频繁切换。切换版本后记得清掉node_modules重装否则会出现“昨天还能跑今天报模块找不到”的玄学问题。3.2 克隆仓库与安装依赖git clone https://github.com/ever-co/ever-gauzy.git cd ever-gauzy # 在根目录安装不要进子目录 yarn install这里有个细节安装过程会触发原生模块编译如果你的机器缺少构建工具链会报node-gyp相关错误。Linux 上需要装build-essential和python3macOS 上需要 Xcode Command Line Tools。我在一台干净的云主机上第一次装就卡在这报了十几行错误其实根因就一个没有编译器。另外仓库提供了示例环境变量文件通常是.env.sample或.env.local这类命名。环境变量文件不提交到版本库需要你自己复制一份改名。这里我建议单独建一个.env.local不要直接改示例文件方便后续对照排查。3.3 数据库初始化与迁移如果用本地 PostgreSQL先建库createdb gauzy然后在环境变量文件里填好连接信息DB_TYPEpostgres DB_HOSTlocalhost DB_PORT5432 DB_NAMEgauzy DB_USERpostgres DB_PASS你的密码接着跑迁移和种子数据yarn migration:run yarn seed:allmigration:run负责把表结构建起来seed:all负责塞入初始数据包括默认管理员账号、基础角色权限、默认租户和组织。这两步的顺序不能反先有表才能灌数据。我见过有人先执行种子脚本结果一堆“relation does not exist”的报错其实就是迁移没跑。种子数据里的默认管理员账号在文档里有说明通常是adminever.co这类固定邮箱密码是常见的默认值。上线前第一件事就是改掉它这一点不用我多说但确实有人忘了。3.4 启动 API 与前端# 终端一启动后端 yarn nx serve api # 终端二启动前端 yarn nx serve gauzy默认情况下 API 监听 3000 端口前端开发服务器在 4200 端口。前端通过代理把/api请求转发到后端具体配置在代理文件里。如果你改了 API 端口记得同步改代理否则前端会一直报 404 或者跨域错误。第一次启动会比较慢Nx 要编译整个依赖链。启动成功后浏览器打开前端地址用种子账号登录如果能看到仪表盘说明主链路通了。这时候别急着配业务数据先在“组织”和“员工”里建几条测试数据把工时记录、发票生成整个流程走一遍验证功能完整性。3.5 用 Docker Compose 一键方案如果你不想折腾本地环境仓库里提供了 Docker Compose 配置通常还区分了开发版和演示版# 演示模式包含数据库、缓存和应用的组合 docker compose -f docker-compose.demo.yml up -d # 查看启动状态与日志 docker compose ps docker compose logs -f apiDocker 方案的好处是环境一致性尤其适合在服务器上快速验证。但有两个点要注意一是镜像首次拉取体积不小网络条件一般的话耐心等二是容器内的数据库默认密码和端口映射跟本地配置不冲突别两个环境混着用搞乱了数据。我个人习惯是本地用原生方式跑方便打断点调试服务器上用 Compose 部署省心。两种方式的环境变量分开维护不要图省事共用一份。4. 集成生态与扩展点怎么用4.1 集成模块的抽象方式ever-gauzy 的集成能力是我比较看重的部分它把外部平台的对接抽象成了一套统一的模型Integration描述“接哪个平台”IntegrationTenant描述“哪个租户接了”IntegrationEntitySetting描述“同步哪些实体、同步方向”。这种三层抽象的好处是新增一个平台不用改核心表只要实现对应的 Provider 逻辑。支持的外部平台涵盖代码托管、任务协作、工时来源等方向。为什么要接这些因为工时的原始数据往往不在管理系统里而在代码提交记录、任务系统或者第三方计时工具里。把外部数据同步进来再结合内部的发票规则才能实现“按实际工作量出账单”这个闭环。这里我要提醒一句集成功能是最容易出权限问题的地方。外部平台通常要求授予一定范围的访问权限建议单独注册一个专用的集成账号只给它必要的读权限别拿管理员账号去接。一是安全二是账号权限变更时能快速定位影响面。4.2 定时同步与增量策略同步一般靠后端的定时任务驱动NestJS 提供Cron之类的装饰器来声明执行周期。这里面有几个实际要考虑的点。第一是增量还是全量。全量同步实现简单但随时间推移越来越慢增量同步需要记录上次同步位置实现复杂但长期更稳。我的经验是第一次接入时先用全量把历史数据拉进来后续切换成按时间窗口的增量窗口长度根据你的数据量调整数据量不大就用最近 24 小时量大就用最近几小时。第二是失败重试。外部接口不会永远可用限流、超时、临时故障都很常见。同步任务必须能记录失败原因并支持重跑否则一次网络抖动就会造成数据缺口。第三是幂等。同一条外部记录被同步两次不应该在系统里产生两条工时记录。这通常要靠外部 ID 做唯一约束来实现。如果你自己写同步逻辑这条务必记住我见过因为没做幂等导致工时翻倍的案例对账时非常痛苦。4.3 自定义扩展的切入点想基于 ever-gauzy 做定制有几个相对安全的切入点。新增业务实体在核心包里按现有模式加实体、DTO、Service、Controller走迁移脚本建表改动路径清晰。新增报表接口单独写一个查询服务注意带上租户和组织过滤避免污染现有逻辑。替换文件存储文件上传相关能力通常通过 Provider 注入可以替换成自己的对象存储实现。调整前端菜单与权限权限体系基于角色和权限点新增页面时同步注册权限点即可。提示改动packages/contracts里的共享类型时一定要全局搜索引用位置把前后端和移动端一起过一遍编译。这是 monorepo 里最容易漏的地方。5. 常见问题与排查技巧实录5.1 依赖安装与构建阶段的坑安装阶段最常见的问题是原生模块编译失败报错里通常出现gyp、python、make等关键词。解决办法是补全构建工具链并固定 Node 版本然后删除node_modules和锁文件缓存重装。别在报错后反复重试同一条命令先把版本对齐。第二个高发问题是 Nx 缓存导致的“假成功”。有时候你改了代码但构建结果没变是因为缓存命中了旧结果。这时可以加--skip-nx-cache参数强制重新构建。我在排查一个前端样式不生效的问题时查了半天才发现是缓存白白浪费了半小时。5.2 数据库与迁移相关报错迁移相关的报错我整理了一张速查表报错现象可能原因处理思路relation does not exist迁移未执行或执行了错误的库确认环境变量指向的库重跑迁移duplicate key value种子数据重复灌入清库后重新初始化或跳过已有种子connection refused数据库服务未启动或端口不对检查服务状态与端口映射permission denied for schema数据库账号权限不足给账号授权或改用高权限账号建表一个实用技巧迁移前先备份。哪怕只是本地环境养成pg_dump的习惯出错时恢复只要一分钟比重新初始化快得多。5.3 前后端联调问题前端打不开接口、报 404 或跨域多半是代理配置和实际 API 地址不一致。排查顺序建议是先直接在浏览器访问 API 的健康检查地址确认后端活着再看前端请求实际发到了哪个地址跟后端监听地址比对最后检查代理配置。令牌过期的表现也容易误判成接口故障。现象是登录后操作一会儿就跳回登录页或者请求返回未授权。这时先确认系统时间和令牌有效期配置时钟偏差会导致令牌被判定为无效这在容器环境里偶尔出现。5.4 一些说明文档里不会写的经验第一条只在需要时才跑完整构建。日常开发用nx serve就够了完整构建留给发布流程能省很多等待时间。第二条日志级别调高一点。默认日志等级下同步、权限校验这些关键环节的信息会缺失排查问题时先把日志级别调成调试级复现一次再调回去。第三条改配置前先记录当前值。环境变量多起来之后很容易改了一处忘了原来是什么出问题时无法回退。我现在的做法是把关键配置整理成一份单独的笔记每次变更都留一行记录看起来很土但救命。第四条别在默认租户上做实验。想测试新功能就新建一个测试租户实验数据和生产数据混在一起清理起来非常麻烦。6. 部署与长期维护上的几点个人体会6.1 单机部署的资源规划如果用 Docker Compose 在一台服务器上部署资源规划要有余量。数据库、缓存、后端服务、前端静态资源加起来2 核 4G 是能跑起来的底线但一旦有几个人同时用构建或者同步任务一起来内存就容易吃紧。我的经验是 4 核 8G 比较舒服磁盘按数据量预估工时和活动记录这类数据增长比想象中快尤其是桌面端采集的活动明细一个月下来量不小。另外建议把数据卷单独挂载容器可以随便重建数据目录一定要落在宿主机上并且定期备份。这算是基本操作但确实有人把数据库文件留在容器里升级镜像时数据一起没了。6.2 多租户下的数据隔离检查前面反复提到租户过滤这里给一个自检思路把所有自定义的查询接口列出来逐个确认是否带了租户条件然后建两个测试租户各自造几条数据用 A 租户的账号尝试访问 B 租户的数据 ID看是否被正确拒绝。这个测试花不了半小时但能挡住最严重的一类问题。我在实际使用中发现ever-gauzy 这类自建平台真正的价值不在于功能比商业产品多多少而在于数据在自己手里、字段能按自己的业务长出来。它不可能开箱即用就完全贴合你的流程前期一定会有一段配置和调整期。我个人的建议是先把最小闭环跑通——记录工时、生成发票、导出对账这三步能顺畅走完再考虑接集成、做定制。反过来先折腾集成和界面很容易在基础数据没理顺的时候就陷进细节里最后不了了之。踩过几次坑之后我越来越觉得选开源系统拼的不是谁功能多而是谁愿意花时间把地基打平。