☰
Ever Gauzy Videos 插件实战指南:基于 @gauzy/plugin-videos 的视频上传、存储与元数据管理
2026/10/1 20:25:57 网站建设 项目流程
  • 后端
  • 前端
  • 企业应用
  • MCP 服务

【免费下载链接】ever-gauzy

Ever® Gauzy™ - Open Business Management Platform (ERP/CRM/HRM/ATS/PM) - https://gauzy.co

项目地址:https://gitcode.com/GitHub_Trending/ev/ever-gauzy
点击查看免费下载

Ever® Gauzy™ 是一个开源的一体化业务管理平台(ERP/CRM/HRM/ATS/PM),在其插件体系中,@gauzy/plugin-videos承担了"视频上传与元数据管理"这一垂直能力。本文以该插件为线索,完整讲解其安装、构建、测试、发布流程,并深入packages/plugins/videos源码剖析其 CQRS 分层、多租户权限模型、存储提供商对接与文件安全防护,帮助读者掌握在 Gauzy 中集成视频管理功能的全链路方案。

插件定位:Gauzy 插件体系中的视频管理模块

@gauzy/plugin-videos是 Gauzy 插件体系中的一个标准库型插件,其 README 明确定义了两个核心能力:

  • 视频上传与存储(Video upload and storage)
  • 视频元数据管理(Video metadata management)

从项目结构看,该插件遵循 Gauzy 统一的插件开发约定(tags: ["type:plugin"],见 project.json),与ai-chat、integration-github、job-proposal等插件并列存放于packages/plugins目录下,通过 Nx 工作区管理。

插件的入口类VideosPlugin使用@GauzyCorePlugin装饰器声明,并实现了IOnPluginBootstrap与IOnPluginDestroy生命周期钩子(见 videos.plugin.ts):

@Plugin({ imports: [VideosModule], entities: [Video], providers: [], exports: [] }) export class VideosPlugin implements IOnPluginBootstrap, IOnPluginDestroy { private logEnabled = true; onPluginBootstrap(): void | Promise<void> { if (this.logEnabled) { console.log(chalk.green(`${VideosPlugin.name} is being bootstrapped...`)); } } onPluginDestroy(): void | Promise<void> { if (this.logEnabled) { console.log(chalk.red(`${VideosPlugin.name} is being destroyed...`)); } } }

VideosPlugin通过imports: [VideosModule]引入业务模块、entities: [Video]注册数据实体,平台启动时自动执行onPluginBootstrap、关闭时执行onPluginDestroy(默认开启绿色/红色日志输出,注释表明该日志开关用于避免事件日志刷屏)。而插件对外的公共 API 面则统一在 index.ts 中导出:

export * from './lib/videos.plugin';

快速开始:安装、构建、测试与发布

安装

README 给出了两种主流包管理器安装方式:

npm install @gauzy/plugin-videos # or yarn add @gauzy/plugin-videos

该包发布到 npm 后,即可作为依赖引入 Gauzy 应用,并在应用模块中注册VideosPlugin。

构建

在仓库内使用 Nx 构建该库:

yarn nx build plugin-videos

构建由@nx/js:tscexecutor 驱动(见 project.json),产物输出到dist/packages/plugins/videos,tsconfig.lib.json指定编译配置,src/index.ts作为入口,同时把packages/plugins/videos/*.md文档一并打包进产物。构建时还会因implicitDependencies声明自动先构建其依赖的contracts、core、plugin库。

运行单元测试

yarn nx test plugin-videos

测试经由@nx/jest:jestexecutor 执行,使用仓库内的 jest.config.ts 配置。现有测试覆盖服务装配与控制器端点(见下文"测试与验证"小节)。

发布

构建完成后进入产物目录执行 npm 发布:

cd dist/packages/plugins/videos npm publish

发布版本号由 Nx 的nx-release-publishtarget 与 git-tag 解析器管理(见 project.json 中release.version.currentVersionResolver: "git-tag")。

架构拆解:NestJS 模块 + CQRS 事件驱动

VideosModule是插件的核心业务装配单元(见 videos.module.ts):

@Module({ controllers: [VideosController], imports: [ TypeOrmModule.forFeature([Video]), MikroOrmModule.forFeature([Video]), RolePermissionModule, CqrsModule ], providers: [ VideosService, VideoSubscriber, TypeOrmVideoRepository, ...CommandHandlers, ...QueryHandlers ], exports: [VideosService] }) export class VideosModule { }

从中可以提取出该插件的分层骨架:

层次代表文件职责
控制器层videos.controller.ts暴露 HTTP 端点,参数校验,权限守卫
命令层(写)commands/CreateVideoCommand/UpdateVideoCommand/DeleteVideoCommand及对应 Handler
查询层(读)queries/GetVideosQuery/GetVideoQuery/GetVideoCountQuery及对应 Handler
服务层services/videos.service.ts继承TenantAwareCrudService<Video>,封装 CRUD
数据访问层repositories/TypeORM 与 MikroORM 双仓储实现
实体层entities/video.entity.ts数据库映射与字段校验
事件层subscribers/video.subscriber.ts实体加载/删除后的存储联动

这种"控制器只做参数与权限处理,业务逻辑通过CommandBus/QueryBus派发到 Handler,Handler 再调用服务层"的 CQRS 模式,与 Gauzy 平台其他模块保持一致:例如GET /列表请求在控制器中仅一行this.queryBus.execute(new GetVideosQuery(params)),POST创建请求执行CreateVideoCommand,读写完全分离。

数据模型:Video 实体与 IVideo 契约

实体字段与校验规则

Video实体继承TenantOrganizationBaseEntity(自带租户与组织归属字段),实现IVideo接口(见 video.model.ts)。核心字段及其校验约束(见 video.entity.ts):

字段类型约束与说明
titlestring必填,3~255 字符,仅允许各语言字母、数字、空格与连字符(/^[\p{L}\p{N}\s-]+$/u)
filestring必填,必须是 MP4 文件名(/^[\w-]+\.(mp4)$/i),实际存储的是文件 key
recordedAtDate可选,ISO 8601 时间字符串,且不得晚于当前时间
durationnumber可选,≥0,单位为秒,存储为real类型
sizenumber可选,0 ~ 10GB(10737418240 字节)上限
fullUrlstring可选,合法的 http/https URL,由订阅器在实体加载时自动生成
descriptionstring可选,≤1000 字符,仅允许字母、数字、空格与基础标点
storageProviderenum可选,取值于FileStorageProviderEnum,序列化时被@Exclude隐藏
resolutionstring可选,格式WIDTH:HEIGHT(如1920:1080),默认VideoResolutionEnum.FullHD
codecstring可选,2~20 字符,默认VideoCodecEnum.libx264
frameRatenumber可选,1~240 fps,默认 15
timeSlotId/uploadedByIdUUID可选,关联TimeSlot与Employee的外键,均建有索引

预定义枚举:分辨率与编码

video.model.ts中定义了两套标准枚举,为元数据提供统一的取值范围:

  • VideoResolutionEnum:覆盖 SD(640:480)、HD(1280:720)、FullHD(1920:1080)、QHD(2560:1440)、UHD4K(3840:2160)、UHD5K、UHD6K、UHD8K、DCI4K(4096:2160)、Cinema2K(2048:1080)、WXGA、SXGA、UXGA、VGA(640:360)、PAL(720:576)、NTSC(720:480)共 16 档。
  • VideoCodecEnum:libx264、libx265、libvpx、libaom、mpeg4、h263、h264、h265、theora、vp8、vp9共 11 种。

实体关系

实体与两个平台核心实体建立多对一关联,且均配置onDelete: 'CASCADE'与@JoinColumn():

  • timeSlot(时间片):视频可归属到某个TimeSlot(时间追踪体系的最小时间单元),关联字段timeSlotId校验 UUID v4;
  • uploadedBy(上传者):记录哪位员工上传了视频,关联字段uploadedById同样校验 UUID v4 并建立索引。

从源码结构看,这两个关系使其天然服务于"员工时间追踪"场景——PermissionsEnum.TIME_TRACKER权限也印证了这一点。

HTTP API 全览:端点、守卫与权限

VideosController以@Controller('/plugins/videos')暴露五个 REST 端点(见 videos.controller.ts):

方法路径说明
GET/plugins/videos分页获取视频列表,支持按时间片/员工筛选(EmployeeTrackedDataGuard)
POST/plugins/videos上传视频文件(multipart/form-data,字段名file)并创建记录
GET/plugins/videos/count统计当前租户内视频数量,可带过滤条件
GET/plugins/videos/:id按 UUID 获取单个视频
PUT/plugins/videos/:id按 UUID 更新视频元数据
DELETE/plugins/videos/:id按 UUID 删除视频(可传软删除选项)

守卫与权限链

控制器类级统一挂载两层守卫与权限声明:

@UseGuards(TenantPermissionGuard, PermissionGuard) @Permissions(PermissionsEnum.TIME_TRACKER)
  • TenantPermissionGuard:校验租户级权限;
  • PermissionGuard+@Permissions(PermissionsEnum.TIME_TRACKER):要求调用者具备时间追踪权限;
  • 列表、详情与计数三个读端点额外挂EmployeeTrackedDataGuard,确保员工只能访问与其追踪记录相关的数据。

所有变更端点(POST/PUT/DELETE)与部分读端点还叠加了UseValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true })——白名单模式会剔除未声明字段、禁止非白名单字段传入,有效防御参数注入。ID 参数统一经UUIDValidationPipe校验。

数据隔离:租户 + 组织 + 员工三级过滤

以列表查询 Handler 为例(见 get-videos.handler.ts),查询逻辑展示了完整的数据隔离策略:

  1. where基础条件固定为tenantId与organizationId;
  2. 若当前用户不具备PermissionsEnum.CHANGE_SELECTED_EMPLOYEE权限,则强制where.uploadedById = RequestContext.currentEmployeeId()——员工只能看到自己上传的视频,无员工上下文时直接返回空分页结果;
  3. 若提供了startDate/endDate,则使用moment-timezone将本地时区(默认UTC)换算为 UTC 后以Between区间过滤recordedAt;
  4. 具备权限的管理者可通过employeeIds数组以In操作符批量过滤上传者;
  5. 关键安全细节:服务端生成的 where 条件排在客户端参数之后(where: { ...params.where, ...where }),以覆盖方式防止调用方通过?where[uploadedById]=<他人ID>越权读取其他员工的录像——源码注释明确指出该缺陷曾可用于读取他人记录。

计数端点getCount(见 get-video-count.handler.ts)执行同样的租户、组织与员工权限校验,并在参数缺失时抛出BadRequestException(组织 ID 与租户 ID 均必填,且无权限时无法确定当前员工也会报错)。

文件上传:安全防线与存储对接

POST创建端点是本插件最复杂的环节,其安全设计值得专门拆解(见 videos.controller.ts)。

上传拦截器

@UseInterceptors( LazyFileInterceptor('file', { storage: () => FileStorageFactory.create('videos'), fileFilter: videoUploadFileFilter }) )
  • LazyFileInterceptor是 Gauzy 核心(@gauzy/core)提供的懒加载文件拦截器,按需创建存储实例;
  • FileStorageFactory.create('videos')按业务目录创建存储提供商实例;
  • videoUploadFileFilter在文件落盘前过滤 MIME 类型。

三道防线的防脚本注入机制

源码注释明确引用了 GHSA-p334-cm7f-php5 类安全通告:视频文件经/public/<key>无鉴权路由按扩展名推断 Content-Type 对外提供,若一个伪装成video/mp4的.svg/.html文件成功落盘,就会在应用同源下执行脚本。为此插件建立三道防线:

  1. 拦截器层:videoUploadFileFilter在接收阶段过滤不合法的视频 MIME;
  2. DTO 层:file.dto.ts 对文件元数据做严格校验——key必须匹配/\.(mp4)$/,mimetype必须匹配/^video\/(mp4)$/,size必须为正数,url上限 2083 字符、path上限 1024 字符;
  3. 字节级复核:shouldScanForMarkup(file.size)判断是否需要对已存储文件内容做标记语言(markup)扫描(大文件因需整体读入内存而跳过),随后assertNotMarkupContent(await provider.getFile(file.key))直接读取已落盘字节重新检查——即使客户端伪造 MIME 与文件名,恶意标记内容也无法在磁盘上存活。

失败清理与字段兜底

创建流程对任何异常都执行provider.deleteFile(file.key)最佳努力清理,避免脏文件残留;成功路径则将tenantId、organizationId、uploadedById从请求上下文兜底(RequestContext.currentTenantId()/currentEmployeeId()),并把storageProvider以大写形式记录入库。最终CreateVideoHandler(见 create-video.handler.ts)把file.key写入Video.file字段后落库。

存储生命周期:FileStorage 与实体订阅器

视频文件的实际读写由 Gauzy 核心的FileStorage抽象负责,它支持多存储提供商(FileStorageProviderEnum),视频插件只需按 key 操作而无需关心底层是本地磁盘、S3 还是其他云存储。

VideoSubscriber(见 subscribers/video.subscriber.ts)以 TypeORMEventSubscriber方式把文件生命周期与数据库事件绑定:

  • afterEntityLoad(加载后):读取实体的storageProvider与file,通过new FileStorage().setProvider(storageProvider).getProviderInstance().url(file)生成完整访问 URL 并写入fullUrl,缺失关键字段时仅记录警告日志;
  • afterEntityDelete(删除后):调用provider.deleteFile(file)同步删除存储中的物理文件,成功删除输出Successfully deleted file for entity ID ...日志,失败则记录错误日志而不阻断删除流程。

这意味着数据库记录与存储文件的生命周期是联动的:删除视频记录会自动回收存储对象,加载记录会自动补全可访问 URL,调用方无需手动管理两处状态。

测试与验证

插件包含两组 Jest 测试,可用yarn nx test plugin-videos运行:

  • videos.service.spec.ts:通过 NestJSTest.createTestingModule注入 TypeORM/MikroORM 仓储 mock,验证VideosService可被正确实例化;
  • videos.controller.spec.ts:验证控制器装配;
  • videos-employee-scope.spec.ts:专门覆盖"员工可见范围"的数据隔离逻辑,呼应上文第 5 节的权限过滤设计。

小结

@gauzy/plugin-videos是一个麻雀虽小五脏俱全的 Gauzy 插件范例:它演示了@Plugin装饰器 + 生命周期钩子的插件接入方式,NestJS 模块与 CQRS 命令/查询的完整分层,TenantOrganizationBaseEntity多租户数据模型与三级数据隔离,以及围绕视频上传构建的多层安全防线(MIME 过滤、DTO 白名单、字节级 markup 扫描、失败文件清理)。如果要在 Ever Gauzy 中为团队增加"录像/演示视频上传与检索"能力,或想参考其模式开发自己的业务插件,本文覆盖的 videos.plugin.ts、videos.controller.ts、video.entity.ts 与 video.subscriber.ts 是最佳的入手路径。

  • 后端
  • 前端
  • 企业应用
  • MCP 服务

【免费下载链接】ever-gauzy

Ever® Gauzy™ - Open Business Management Platform (ERP/CRM/HRM/ATS/PM) - https://gauzy.co

项目地址:https://gitcode.com/GitHub_Trending/ev/ever-gauzy
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询