跳到主要内容

Swee-面试高频问题与回答思路

Swee 是一个 AI 驱动的社交娱乐平台,包含管理后台(UmiJS)、移动端 PWA(Next.js)、后端服务(NestJS)、官网(Next.js)和 H5 页面(Vite)。以下问答覆盖全栈架构设计、核心技术实现、数据库设计、性能优化等面试高频方向。


一、后端架构类

Q1:NestJS 后端的整体架构是怎么设计的?

:后端基于 NestJS 10 + TypeORM + MySQL 构建,采用模块化分层架构。

模块划分:按业务域拆分为 5 个核心模块:

  • Copywriting 模块:多语言文案管理(CRUD + 批量导入/导出 + 微软翻译 API 集成)
  • File 模块:文件上传(阿里云 OSS 直传 + CDN 自动刷新)
  • Setting 模块:配置管理(JSON 配置 + 版本控制 + 回滚)
  • Material 模块:素材库(Materialized Path 树形结构 + 文件夹导航)
  • Task 模块:定时任务(9 大新闻源聚合 + Levenshtein 去重 + 飞书推送)

请求处理管道:每个请求经过完整的管道式处理:

NestJS 请求处理管道
请求 → TraceMiddleware → AuthGuard → ValidationPipe → Handler → ResponseInterceptor → AllExceptionsFilter → 响应
(链路追踪ID) (权限校验) (参数校验) (业务处理) (响应格式统一) (异常兜底)
面试加分项

这套管道的好处是新增 API 自动享有完整保护——开发者只需写 Controller 和 Service,链路追踪、权限校验、参数验证、响应格式化、异常处理全部由管道自动完成。

追问:TraceMiddleware 是怎么实现链路追踪的?

TraceMiddleware 在管道最前端为每个请求生成唯一的 traceId(UUID),注入到 request 对象中。后续的所有日志、错误上报都带上这个 traceIdResponseInterceptor 在响应体中也返回 traceId,前端遇到问题时可以拿着 traceId 让后端定位日志,实现前后端联动排查。


Q2:权限控制是怎么设计的?

:我设计了一套三级权限体系,从前端到后端层层拦截:

三级权限体系
第一级:路由级权限(前端 UmiJS access 插件)
→ 用户登录后拉取权限列表,access 插件自动过滤菜单和路由
→ 无权限的页面直接不显示在菜单中,URL 直接访问也会 403

第二级:组件级权限(前端 useAccess Hook)
→ 页面内部的操作按钮(删除、编辑、导出等)通过 useAccess() 判断显隐
→ 有查看权限但无编辑权限的用户,只能看不能改

第三级:API 级权限(后端 @Auth 装饰器)
→ 即使前端被绕过,后端最终拦截
→ @Auth({ type: 'admin', permissionId: 'system:copywriting:collection' })

@Auth 装饰器的实现原理: 它是一个自定义的复合装饰器,内部组合了 @UseGuards(AuthAdminGuard)@SetMetadata('permissionId', ...)。AuthGuard 执行时:

  1. 从请求头提取 Token
  2. 调用外部用户服务验证 Token 并获取用户信息和权限列表
  3. 从 Reflector 读取 permissionId 元数据
  4. 比对用户权限列表是否包含该 permissionId
  5. 通过后将用户信息注入到 request.adminUserInfo,Service 层可直接使用
设计思路

Controller 只需一行装饰器声明权限,业务代码完全不用关心权限逻辑。这是 NestJS 装饰器模式的典型应用。

追问:这个设计有什么不足?

有一个性能瓶颈:每个 API 请求都远程调用外部 Java 服务来验证 Token。高并发时每个请求额外增加一次网络往返(10-50ms)。改进方案是:

  1. 本地 JWT 验证:Java 服务签发 JWT,NestJS 本地验证签名,无需远程调用
  2. 用户信息缓存:验证后缓存用户信息(Redis / 内存,TTL 5 分钟),相同 Token 后续请求命中缓存

Q3:多环境配置是怎么管理的?

:采用「通用基础 + 环境覆盖」的深度合并策略

多环境配置合并流程
common.config.ts(通用配置:OSS 区域、端口、API URL 模板...)
↓ mergeDeep()
development.config.ts / test.config.ts / production.config.ts

最终运行时配置

关键设计点

  1. 深度合并而非浅层覆盖common.config.ts 定义完整的配置树,环境配置只覆盖需要差异化的字段。比如数据库配置在 common 中定义了 host、port、charset 等通用项,production 只覆盖 host 和 password
  2. TConfig 类型约束:导出统一的 TypeScript 类型,Service 中使用 config.get('oss.accessKeyId', { infer: true }) 获取配置项时有完整的类型提示
  3. 环境切换:通过 NEST_ENV 环境变量控制,Docker 构建时注入,不同部署环境不需要修改代码

追问:这个方案的安全隐患是什么?

安全隐患

当时的实现是把密钥(数据库密码、OSS Key、API Key)直接写在了 .ts 配置文件中,这些文件会被 Git 追踪,等于密钥提交到了代码仓库。这是一个严重的安全问题。

正确做法是将密钥读取改为 process.env.*,配合 @nestjs/configConfigModule.forRoot().env 文件加载。.env 必须在 .gitignore 中,只提交 .env.example 模板。不同环境的密钥注入方式:

  • 本地开发:.env 文件(gitignore)
  • CI/CD:GitHub Secrets / GitLab CI Variables
  • K8s 生产环境:kubectl create secret

Q4:你怎么设计 RESTful API?当前项目的 API 有什么问题?

:当前项目的 API 设计偏离了 RESTful 原则,所有操作都使用 POST 方法 + 动词命名:

当前 API 设计(不规范)
POST /copywriting/collection/admin/add_collection      → 创建
POST /copywriting/collection/admin/delete_collection → 删除
POST /copywriting/collection/admin/get_collection_list → 查询(也是 POST)

问题

  • HTTP 方法的语义(GET/POST/PUT/DELETE)完全丢失
  • CDN 和浏览器无法对 POST 请求做缓存
  • 动词命名和 HTTP 方法语义重复
  • 不支持 API 版本管理

如果重新设计,会遵循 RESTful 规范:

改进后的 RESTful API 设计
POST   /api/v1/admin/copywriting/collections          → 创建
DELETE /api/v1/admin/copywriting/collections/:id → 删除
PATCH /api/v1/admin/copywriting/collections/:id → 更新
GET /api/v1/admin/copywriting/collections → 列表查询
GET /api/v1/admin/copywriting/collections/:id → 单个查询

核心原则:资源用名词复数,HTTP 方法表示操作类型,路径参数表示资源 ID,查询参数用于筛选和分页,/api/v1/ 前缀支持版本管理。

NestJS 原生支持版本管理:app.enableVersioning({ type: VersioningType.URI }) + @Controller({ path: 'copywriting', version: '1' })


Q5:定时任务(新闻聚合)是怎么设计的?

:这是一个完整的新闻聚合定时任务系统,每天自动从 9 个新闻源 抓取内容,经过去重、翻译后推送到飞书。

流程

新闻聚合定时任务流程
@Cron('0 0 * * *')  →  9 个新闻源并行抓取

Cheerio HTML 解析 → 提取标题/内容/图片

Levenshtein 距离去重(0.25 阈值 + 7 天缓存窗口)

微软翻译 API 多语言翻译

存储到数据库

飞书 Bot 推送(Markdown 卡片)+ 写入云文档

Levenshtein 去重原理: 对比两篇新闻标题的编辑距离(将一个字符串转换为另一个所需的最少操作次数),归一化后得到相似度。如果相似度 > 0.75(即编辑距离 < 0.25),判定为重复。同时维护 7 天的缓存窗口,只与近期新闻比较,避免性能问题。

追问:这个设计有什么架构问题?

有几个问题:

  1. 同步阻塞:9 个新闻源抓取 + 翻译 + 飞书推送全在一个同步方法中执行,任何一步超时或失败都会中断整个流程
  2. 无分布式锁:多实例部署时每个实例都会执行定时任务,导致重复抓取和推送
  3. 无熔断:微软翻译 API 或飞书 API 故障时没有降级策略

改进方案

  • 引入 BullMQ 消息队列,定时任务只负责发消息,Worker 异步消费
  • 用 Redis SETNX 做分布式锁,确保只有一个实例执行
  • 外部 API 调用加熔断器(连续失败 5 次 → 熔断 30 秒 → 降级)

二、核心技术实现类

Q6:VTable 大规模多语言编辑方案是怎么实现的?为什么从 V1 升级到 V2?

核心回答要点

这是整个 Swee 项目最具工程思考的技术决策——从发现 DOM 瓶颈到 Canvas 渲染方案的选型过程,体现了完整的性能工程思维。

V1 方案(Ant Design EditableProTable)

  • 基于 DOM 渲染,每个单元格是一个真实的 <td> + <input> 元素
  • 当数据量达到约 1000 行 × 8 语言 = 8000 个单元格时,DOM 操作开始明显卡顿
  • 滚动、编辑、保存都有肉眼可见的延迟

V2 方案(VTable Canvas 渲染)

  • 基于 Canvas 绘制,所有单元格都是画布上的图形,不产生 DOM 节点
  • 只渲染可视区域 + 小范围缓冲区(类似虚拟列表的思想)
  • 10000+ 行流畅编辑,性能提升约 10 倍

关键技术点

  1. React in Canvas:通过 VTable 的 <ListColumn> 组件 + react 属性,在 Canvas 单元格中注入 React 操作按钮(编辑、删除),实现了 Canvas 高性能 + React 交互灵活性 的融合
  2. 自定义 TextAreaEditorWithValidator:继承 VTable 的编辑器接口,实现单元格级验证 + 三色状态标记(新增绿色、编辑蓝色、错误红色)
  3. 微软翻译器集成:选中原文一键翻译为 8 种目标语言,减少运营人员的翻译负担
  4. 字符超长检测:每种语言的文案有字符数限制,编辑时实时检测并标红

追问:Canvas 渲染和 DOM 渲染的本质区别是什么?

DOM 渲染:每个单元格是一个 HTML 元素(有自己的盒模型、事件绑定、样式计算),浏览器需要维护 DOM 树 + CSSOM 树 + Layout 树。8000 个元素意味着 8000 次 Layout 计算。

Canvas 渲染:整个表格是一个 <canvas> 元素,单元格只是画布上的像素绘制。滚动时重新绘制可视区域,不需要 DOM 操作。事件通过坐标计算映射到对应单元格(Hit Test)。

本质差异:DOM 是声明式的(告诉浏览器"有 8000 个元素"),Canvas 是命令式的(告诉浏览器"画这些像素")。当元素数量大时,命令式更高效。


Q7:配置管理的可视化编辑器是怎么实现智能类型推断的?

:核心思路是根据 JSON 值的内容自动推断 UI 组件类型

JSON 值智能类型推断规则
JSON 值 → 正则匹配判断类型 → 动态渲染对应组件

"https://cdn.xxx.com/a.png" → 匹配图片后缀 → 图片预览 + 上传组件
"https://cdn.xxx.com/b.mp4" → 匹配视频后缀 → 视频播放器 + 上传组件
"https://cdn.xxx.com/c.mp3" → 匹配音频后缀 → 音频播放器 + 上传组件
"2024-01-01T00:00:00Z" → 匹配 ISO 日期 → 日期选择器
"#FF5733" → 匹配颜色值 → 颜色选择器
其他字符串 → 默认 → 文本输入框

设计优势:运营人员不需要手动配置每个字段的类型,系统自动识别。当 JSON 配置增加新的图片字段时,编辑器自动展示预览和上传功能,零配置。

另外还支持 JSONEditor 6 种编辑模式(tree/view/form/code/text/preview),不同复杂度的配置场景可以切换合适的编辑模式。配置还有版本回退功能,通过 lastContent 字段保存上一版,误操作可以一键回滚。


Q8:OpenAPI 代码生成是怎么工作的?

:实现了从后端 DTO 到前端 API 客户端的全链路自动化

流程

OpenAPI 全链路自动化流程
后端 DTO + @ApiProperty() 装饰器

NestJS Swagger 插件 → 自动生成 Swagger JSON(/api-docs)

前端 pnpm openapi → @umijs/openapi 读取 Swagger JSON

自动生成 TypeScript API 函数 + 类型定义

实际效果

OpenAPI codegen 示例
// 后端 DTO(唯一数据源)
export class CreateCollectionDto {
@ApiProperty({ description: '集合名称' })
@IsString()
name: string;
}

// 前端自动生成(无需手写)
export async function addCollection(body: CreateCollectionDto) {
return request<ApiResponse<Collection>>('/copywriting/collection/admin/add_collection', {
method: 'POST',
data: body,
});
}

好处

  1. 后端改了 DTO → 前端 pnpm openapi → TypeScript 编译报错 → 精确定位需要适配的地方
  2. swee-mobile 同时对接 6 个 Java 微服务(user、recommend、live、model、interact、im),全部通过 codegen 生成,节省大量手写 API 代码的工作
  3. 保证前后端类型 100% 一致

Q9:PWA 方案是怎么设计的?

:swee-mobile 使用 next-pwa 实现 PWA 支持,配置了完整的 Service Worker 和 Web App Manifest。

配置要点

  • manifest.jsondisplay: "standalone" 实现全屏 App 体验、深色主题、应用图标和安装截图
  • Service Worker:自动注册 + skipWaiting(有新版本时立即更新,不等用户关闭)
  • 多入口适配:同时支持 App WebView 内嵌、PWA 独立模式、移动浏览器访问

支持的场景

  1. 用户在手机浏览器中访问 → 提示"添加到主屏幕"
  2. 添加后以 standalone 模式运行,看起来和原生 App 一样(隐藏浏览器地址栏)
  3. App 内嵌 WebView 也能正常运行

追问:PWA 方案有什么不足?

  1. manifest.json 图标缺少 maskable 变体,Android 设备上图标显示效果不够好
  2. Service Worker 使用默认缓存策略,没有针对 API 请求和图片做差异化缓存(API 应该用 NetworkFirst,图片应该用 CacheFirst)
  3. 没有离线页面(离线时应该展示友好的提示页,而非浏览器默认的离线错误)

Q10:Easemob IM 即时通讯是怎么集成的?

:swee-mobile 使用 Easemob WebSDK 4.13 的 MiniCore 轻量模式 集成即时通讯功能。

为什么选 MiniCore? 标准 SDK 包含音视频通话、群组管理等完整功能,体积大。MiniCore 只保留核心的消息收发能力,适合我们「AI 伴侣对话」这种以文本为主的场景。

架构设计

Easemob IM 架构设计
EasemobProvider(全局 Context)
├── SDK 初始化 + 连接管理
├── 消息监听 + 统一转换
├── 用户信息缓存(Zustand Store)
└── 对外暴露 sendMessage / fetchHistory 等方法

消息转换管线:Easemob 原始消息格式和我们的 UI 展示格式不同,中间有一层转换逻辑,将不同类型的消息(文本、图片、自定义卡片)统一格式化为 UI 组件可以直接渲染的结构。

用户信息缓存:使用 Zustand Store 缓存已查询过的用户信息,避免每次展示消息时都调用 API 查头像和昵称。


三、数据库与性能类

Q11:数据库 Entity 是怎么设计的?

:所有实体统一实现了软删除 + 时间戳审计

BaseEntity 审计字段
@CreateDateColumn() createdAt: Date;   // 自动记录创建时间
@UpdateDateColumn() updatedAt: Date; // 自动记录更新时间
@DeleteDateColumn() deletedAt: Date; // 软删除(非空 = 已删除)

软删除的好处是数据不会真正删除,可以恢复。TypeORM 的 @DeleteDateColumn 会自动在查询时过滤已删除的记录(WHERE deleted_at IS NULL),业务代码无需关心。

另外有一个独立的 LogEntity 记录操作审计日志:操作人、操作 IP、操作类型、变更前后的 JSON 对比。这样任何配置变更都有完整的追溯链。

追问:Entity 设计有什么不足?

主要有两个问题:

  1. 缺少索引:所有实体都没有 @Index() 装饰器。copywriting.entitykey 字段、setting.entityname 字段都是高频查询字段,没有索引会随着数据量增长产生性能问题。

  2. 无数据库迁移:开发环境使用 synchronize: true 自动同步 schema,但整个项目没有迁移文件。生产环境的 schema 变更只能手动执行 SQL,多人开发时容易冲突,回滚困难。应该使用 TypeORM 的 migration:generatemigration:run 管理。


Q12:N+1 查询问题是怎么产生的?怎么解决?

:在文案管理的 getCopywritingListByCollections 方法中,需要根据多个 collectionId 查询对应的文案列表。

N+1 问题

N+1 查询问题示例
// 当前实现 — N+1 查询
for (const collectionId of collectionIds) {
const items = await this.copywritingRepository.find({
where: { collection: { id: collectionId } }
});
results.push(...items);
}
// 如果有 10 个 collectionId,就发 10 次 SQL 查询

解决方案:使用 TypeORM 的 In() 操作符,一次查询所有:

使用 In() 批量查询
const items = await this.copywritingRepository.find({
where: { collection: { id: In(collectionIds) } }
});
// 无论多少个 collectionId,只有 1 次 SQL 查询
// SELECT * FROM copywriting WHERE collection_id IN (?, ?, ?, ...)

N+1 的本质:循环中的每次迭代都发起一次数据库查询,导致查询次数 = 1(主查询)+ N(循环查询)。解决思路是将循环查询合并为一次批量查询。


Q13:Materialized Path 树形结构是怎么实现的?

:素材库的文件夹采用 Materialized Path(物化路径) 算法实现树形结构。

原理:每个节点存储从根到自身的完整路径,用分隔符连接:

Materialized Path 树形结构示例
根文件夹      path = "1"
├── 图片 path = "1.2"
│ ├── 封面 path = "1.2.5"
│ └── 头像 path = "1.2.6"
└── 视频 path = "1.3"

核心操作的 SQL

  • 查询子节点:WHERE path LIKE '1.2.%'(查 "图片" 下的所有子文件夹)
  • 查询祖先链:解析 path 字符串 "1.2.5" → 查询 id IN (1, 2, 5) 得到面包屑
  • 移动节点:更新自身及所有子节点的 path 前缀
  • 删除节点:DELETE WHERE path LIKE '1.2.%'(级联删除所有子节点)

对比其他方案

方案查询子树移动节点适用场景
邻接表(parent_id)递归查询,性能差简单层级少
Materialized PathLIKE 查询,简单高效需更新所有子节点路径读多写少
嵌套集(左右值)范围查询,最快需重算大量左右值极少变更

素材库是典型的「读多写少」场景(浏览远多于创建/移动),所以 Materialized Path 是最合适的选择。


Q14:缓存是怎么设计的?

:使用 NestJS 的 @nestjs/cache-manager,通过 @CacheInterceptor 按路由粒度控制缓存。

当前实现

内存缓存示例
@UseInterceptors(CacheInterceptor)
@CacheTTL(60) // 60 秒缓存
@Get('config')
getConfig() { ... }

问题:使用默认的内存缓存(CacheModule.register() 无 store 配置),存在两个问题:

  1. 进程重启缓存丢失:NestJS 重启后缓存清空,需要重新预热
  2. 多实例无法共享:如果部署了 2 个 Pod,各自维护独立缓存,数据不一致

改进方案:接入 Redis 作为缓存后端:

Redis 缓存改进方案
CacheModule.register({
store: redisStore,
host: process.env.REDIS_HOST,
ttl: 60,
});

Redis 作为独立服务,所有实例共享同一份缓存,且进程重启不丢失。


四、前端架构类

Q15:swee-mobile 的状态管理是怎么设计的?

:使用 Zustand 5.0 做状态管理,选择它而非 Redux 的原因是:

  • 体积极小(~1KB),适合移动端 PWA
  • API 简洁,无 Action/Reducer 模板代码
  • 支持中间件(persist、devtools)

当前使用方式:主要用于 IM 用户信息缓存——Easemob 消息只包含用户 ID,展示时需要查询用户信息(头像、昵称)。使用 Zustand Store 缓存查询结果,避免重复 API 请求。

追问:当前的 Zustand 设计有什么不足?

几个问题:

  1. Map 类型不可序列化userInfoMap 使用 new Map() 存储,但 Map 不支持 JSON 序列化,DevTools 无法查看,persist 中间件也无法持久化。应该改为 Record<string, UserInfo>
  2. 整个应用只有一个 Store:应该按业务域拆分(useIMStoreuseUserStoreuseAIStore
  3. 无持久化:用户信息缓存应该用 zustand/middlewarepersist 中间件存到 localStorage,避免每次打开 App 重新请求

Q16:Framer Motion 页面转场动画是怎么实现的?

:swee-mobile 使用 Framer Motion 的 AnimatePresence 实现原生 App 级别的页面切换动画。

实现方式

pages/_app.tsx
<AnimatePresence mode="wait">
<motion.div
key={router.pathname}
initial={{ x: '100%', opacity: 0 }} // 新页面从右侧滑入
animate={{ x: 0, opacity: 1 }} // 定位到中间
exit={{ x: '-30%', opacity: 0 }} // 旧页面向左滑出
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<Component {...pageProps} />
</motion.div>
</AnimatePresence>

关键点

  • key={router.pathname} 告诉 AnimatePresence 路由变化 = 组件切换
  • mode="wait" 确保旧页面退出动画完成后新页面才进入
  • 动画在 GPU 上执行(transform: translateX),不触发 Layout/Paint

这让 PWA 的页面切换体验接近原生 App,用户感觉不到是 Web 应用。


Q17:三平台埋点事件管理系统是怎么设计的?

:swee-admin 中有一个完整的埋点事件管理系统,通过三层嵌套编辑实现:

三层嵌套编辑结构
第一层:ModalForm — 事件基本信息(事件名、描述、所属模块)
└── 第二层:EditableProTable — 事件属性列表(属性名、类型、是否必须)
└── 第三层:嵌套 EditableProTable — 枚举值列表(枚举名、枚举值、描述)

设计亮点

  1. 动态列生成:事件的上报数据是动态结构,后台根据事件定义自动生成查看表格的列
  2. @swee/codegen 集成:埋点事件定义完成后,通过代码生成工具自动生成前端上报代码,减少手写工作量
  3. 三层嵌套编辑的 UX:虽然数据结构复杂,但用户操作是层层展开的自然流程

Q18:VTable 大规模编辑中怎么处理用户体验?

:除了 Canvas 渲染解决性能问题外,我们在 UX 上也做了细致设计:

  1. 三色状态标记

    • 绿色底色 = 新增的文案(还未保存)
    • 蓝色底色 = 修改过的文案(与原始值不同)
    • 红色边框 = 验证不通过(超长、格式错误)
  2. 字符计数实时反馈:编辑器底部显示当前字数 / 最大字数,超长时实时变红

  3. 一键翻译:选中中文原文,点击翻译按钮,微软翻译 API 同时生成 8 种语言的翻译,填入对应列

  4. 批量操作:支持 Excel 格式导入/导出,运营人员可以在 Excel 中批量编辑后导入

  5. 撤销/重做:VTable 内置的编辑历史支持 Ctrl+Z / Ctrl+Y


五、问题排查与改进类

Q19:项目中遇到过哪些安全问题?

:做代码审查时发现了多个安全问题:

最严重的:后端 6 个生产密钥(数据库密码、OSS AccessKey、微软翻译 Key、飞书 Secret)明文硬编码在 .ts 配置文件中,等于所有有代码读取权限的人都能访问生产数据库。

其他安全问题

  1. Token 日志泄露:Guard 中 console.log(accessToken) 将用户 Token 写入服务器日志
  2. CORS 全开cors: true 允许任意域名跨域请求
  3. 无 Helmet:缺少 HTTP 安全头(X-Content-Type-Options、X-Frame-Options、CSP)
  4. 无速率限制:没有 ThrottlerModule,暴力破解和 DDoS 无防护
  5. 硬编码鉴权:一个下载接口用固定字符串做 Token 校验
  6. 超级管理员 userId===1:硬编码 ID 判断超级权限

修复方案

  • P0:立即轮换所有泄露密钥,迁移到环境变量
  • P0:移除 Token 日志,配置 CORS 白名单,添加 Helmet 和 ThrottlerModule
  • P1:硬编码鉴权改为 Guard 装饰器,userId 判断改为 RBAC 角色系统

Q20:HTTP 状态码全部返回 200 有什么问题?

:当前后端所有业务错误都返回 HTTP 200,通过响应体的 code 字段区分成功和失败。

HTTP 状态码滥用示例
// 当前实现
super({ code: 10001, message: '参数错误' }, HttpStatus.OK); // HTTP 200
super({ code: 10002, message: '未授权' }, HttpStatus.OK); // 也是 HTTP 200

问题

  1. 监控盲区:Nginx、CDN、APM 工具都通过 HTTP 状态码统计错误率。全部 200 = 错误率永远是 0%
  2. 前端无法统一拦截:Axios 的 interceptors.response.error 只捕获非 2xx 状态码,全部 200 意味着错误处理只能在每个请求的 .then() 中手动判断 code
  3. 缓存问题:HTTP 缓存机制基于状态码,错误响应不应该被缓存

正确做法:按错误类型返回对应的 HTTP 状态码:

  • 400 Bad Request — 参数错误
  • 401 Unauthorized — 未认证
  • 403 Forbidden — 无权限
  • 404 Not Found — 资源不存在
  • 500 Internal Server Error — 服务器内部错误

Q21:如果你要给这个项目添加事件驱动架构,会怎么设计?

:当前所有操作都是同步 HTTP 请求-响应模式,长耗时操作(新闻抓取、批量翻译、OSS 上传)直接阻塞连接。

设计方案:引入 BullMQ(基于 Redis 的消息队列):

BullMQ 事件驱动架构
                               ┌→ 新闻抓取 Worker
API → 发送消息到队列 → Redis ──┼→ 翻译 Worker
返回任务 ID └→ 飞书推送 Worker

前端通过任务 ID 轮询状态 或 WebSocket 接收完成通知

NestJS 中的实现

NestJS BullMQ 生产者/消费者
// Producer(Controller)
@Post('batch-translate')
async batchTranslate(@Body() dto: BatchTranslateDto) {
const job = await this.translateQueue.add('translate', dto);
return { taskId: job.id, status: 'processing' };
}

// Consumer(Worker)
@Processor('translate')
export class TranslateProcessor {
@Process('translate')
async handleTranslate(job: Job<BatchTranslateDto>) {
// 异步执行翻译
await this.translateService.batchTranslate(job.data);
// 完成后可以通过 WebSocket / 飞书 通知
}
}

好处

  • API 立即响应(用户不用等),Worker 异步处理
  • 失败自动重试(BullMQ 内置重试机制)
  • 可以监控队列长度和处理速度
  • 多实例环境下 Worker 自动负载均衡

六、技术选型与权衡类

Q22:为什么选 NestJS 而不是 Express 或 Koa?

:NestJS 的核心优势在于它是一个有架构约束的框架,而 Express/Koa 只是 HTTP 库。

具体原因:

  1. 模块化架构:NestJS 的 Module/Controller/Service/Provider 模式强制了代码组织规范,团队多人协作时代码结构一致
  2. 装饰器驱动@Controller@Guard@Interceptor 等装饰器让代码更声明式,比 Express 的 app.use() 中间件链更清晰
  3. 依赖注入:Service 之间通过构造函数注入,方便单元测试(可以 mock 依赖)
  4. 内置集成:TypeORM、Swagger、Cache、Schedule、Throttle 等都有官方模块,不需要自己选型和集成
  5. TypeScript 优先:从设计之初就是 TypeScript,类型支持完善

Express/Koa 更适合轻量的 API 服务或需要极致灵活性的场景。我们的后端有多个业务模块、需要权限控制、定时任务、缓存等功能,NestJS 的架构约束反而是优势。


Q23:VTable(Canvas)vs EditableProTable(DOM),你是怎么做技术决策的?

:决策过程:

  1. 明确瓶颈:用 Chrome Performance 面板分析发现,EditableProTable 在 1000+ 行时,滚动帧率从 60fps 降到 15fps。主要耗时在 Layout 和 Paint——8000 个 DOM 节点的重排重绘

  2. 评估方案

    • 方案 A:虚拟滚动(react-virtualized)— 只渲染可视区域的 DOM 节点。但 EditableProTable 的编辑态需要维护每个单元格的状态,虚拟滚动时滚出视口的编辑状态会丢失
    • 方案 B:Canvas 表格(VTable)— 根本性解决 DOM 瓶颈,10K+ 行无压力。但需要解决 Canvas 中的交互问题(编辑器、按钮)
  3. 选择 VTable 的原因

    • 性能数量级提升(方案 B >> 方案 A)
    • VTable 支持 "React in Canvas",可以在 Canvas 单元格中注入 React 组件,解决交互问题
    • 字节跳动(VisActor)团队维护,质量和迭代可靠
  4. 迁移策略:V1(EditableProTable)和 V2(VTable)并行存在一段时间,通过特性开关切换,确认 V2 稳定后再下线 V1


Q24:你觉得 Swee 项目中最有价值的技术实现是什么?

VTable 大规模多语言编辑方案

这不只是"换了一个表格组件"的事情,而是一次完整的性能工程实践

  1. 发现问题:通过性能分析定位到 DOM 渲染瓶颈
  2. 技术评估:对比虚拟滚动和 Canvas 两种方案
  3. 技术融合:React in Canvas 实现了"Canvas 高性能 + React 灵活交互"
  4. 产品体验:三色状态标记、一键翻译、字符检测等 UX 细节

最终效果是从「1000 行卡顿」到「10000+ 行流畅」的数量级提升,直接解放了运营团队的生产力。


Q25:如果让你从零重新设计 Swee 后端,你会怎么做?

:保持不变的部分:

  • NestJS 模块化架构 + TypeORM
  • 管道式请求处理链(Middleware → Guard → Pipe → Handler → Interceptor → Filter)
  • @Auth 装饰器的声明式权限控制
  • OpenAPI 全链路自动化

会改进的部分:

  1. 密钥管理:从第一天起就用环境变量 + .env.example,绝不在源码中硬编码
  2. RESTful API:严格遵循 HTTP 方法语义 + 资源名词命名 + 版本管理
  3. JWT 本地验证:替代每请求远程校验,加用户信息缓存
  4. Redis:作为缓存后端(替代内存缓存)+ 消息队列后端(BullMQ)+ 分布式锁
  5. 数据库迁移:从第一天起使用 TypeORM Migration,关闭 synchronize
  6. 严格 TypeScript:启用 strict: true
  7. 测试:核心 Service 100% 单元测试覆盖
  8. 健康检查/health + /ready 端点 + app.enableShutdownHooks() 优雅关机
  9. 日志系统:NestJS Logger 替代 console.log,按级别过滤

七、综合能力类

Q26:你在这个项目中踩过最大的坑是什么?

VTable V1 到 V2 的迁移过程

V1 使用 Ant Design 的 EditableProTable,我们在它基础上做了大量定制(三色状态标记、字符验证、一键翻译按钮等)。迁移到 VTable 时,这些定制功能需要全部用 VTable 的 API 重新实现。

最大的挑战是 React in Canvas——在 Canvas 单元格中嵌入 React 操作按钮。VTable 的渲染引擎和 React 的渲染树是两个独立系统,需要通过 VTable Group 组件 + react 属性建立桥接。调试起来也很困难,因为 Canvas 元素在 React DevTools 中看不到。

解决方法是:

  1. 先实现纯数据展示(Canvas 渲染),确认性能
  2. 再逐步添加交互功能(编辑器、按钮),每添加一个功能都做性能回归
  3. V1 和 V2 并行运行一段时间,确认稳定后下线 V1

Q27:你对 TypeORM 的使用有什么心得?有什么注意事项?

:TypeORM 的优点是 Active Record / Data Mapper 两种模式灵活选择,装饰器语法和 NestJS 风格一致。

注意事项

  1. 不要在生产用 synchronize: true:它会自动修改数据库 schema,可能导致数据丢失。应该用 Migration

  2. 关注 N+1 问题:关联关系默认是懒加载,循环中访问关联属性会产生 N+1 查询。解决方案:find() 时用 relations 选项或 QueryBuilder 的 leftJoinAndSelect

  3. 索引要加:TypeORM 不会自动为频繁查询的字段加索引,需要手动 @Index() 装饰器

  4. 软删除配合@DeleteDateColumn + softRemove() / softDelete() 实现软删除,但查询时 TypeORM 会自动加 WHERE deleted_at IS NULL,如果需要查已删除数据要用 withDeleted() 选项

  5. QueryBuilder 更灵活:简单查询用 find() / findOne(),复杂查询(JOIN、子查询、聚合)用 QueryBuilder


Q28:你们怎么处理前后端联调?遇到类型不一致的问题怎么办?

:我们通过 OpenAPI codegen 最大化减少联调问题。流程是:

  1. 后端写好 DTO 和 Controller → Swagger 自动生成 API 文档
  2. 前端执行 pnpm openapi → 自动生成 TypeScript API 函数和类型
  3. 前端直接使用生成的函数调用 API,类型由 codegen 保证一致

类型不一致的情况

  • 如果后端改了 DTO 但没通知前端 → 前端重新 pnpm openapi 后 TypeScript 编译报错 → 精确定位需要适配的代码
  • 如果是对接外部 Java 微服务(不用 NestJS Swagger)→ 需要从 Java 服务的 Swagger 文档生成,偶尔有字段类型不一致的情况,需要手动修正

最佳实践:后端 DTO 的每个字段都加 @ApiProperty({ description: '...', example: '...' }),这样生成的文档和 TypeScript 类型都更完善。


Q29:你对代码质量和工程规范有什么看法?

:基于 Swee 项目的经验,我认为几个关键的工程规范:

  1. 安全是底线:密钥绝不能出现在源码中,这是第一天就应该建立的规范。我们项目因为早期没注意,导致了严重的安全隐患

  2. 测试不是可选项:Swee 后端的核心 Service(文案管理、新闻聚合、配置管理)没有任何测试,每次修改都是"盲改"。至少核心模块应该有单元测试覆盖

  3. 日志要规范console.log(2222) 这种调试代码不应该出现在主分支。应该用 NestJS Logger,生产环境只输出 warn/error 级别

  4. TypeScript strict:后端没有启用 strict 模式,允许隐式 any,失去了 TypeScript 的核心价值

  5. 数据库变更要可追溯synchronize: true 在生产是定时炸弹,应该从项目初始化就用 Migration

核心教训

这些问题不是不知道,而是在快速迭代中被忽略了。工程规范应该在项目初始化时就建立,而不是等出了问题再补。初期多投入 1 天建立规范,能避免后期几周的修复成本。


Q30:如果面试官问你这个项目的不足,你会怎么回答?

:我会坦诚地分层说明:

安全层面(最严重):

  • 6 个生产密钥硬编码在源码中
  • CORS 全开 + 无速率限制 + Token 日志泄露
  • 这些问题说明团队的安全编码意识需要加强

质量保障层面

  • 测试覆盖接近零
  • Sentry 错误监控被注释
  • 前端无 ErrorBoundary
  • 说明质量保障体系不完善

架构层面

  • API 设计偏离 RESTful(全 POST + 动词命名)
  • 每请求远程认证(性能瓶颈)
  • 无事件驱动架构(同步耦合过重)
  • 无 API 版本管理
面试回答策略

发现问题并知道怎么改进,比假装没有问题更有价值。坦诚说明不足的同时,展示你的分析能力和改进方案,面试官会更认可你的工程素养。

但我会强调

  1. 这些问题我都已经识别到了,并且有明确的改进方案和优先级排序
  2. 安全问题我归类为 P0 需要立即修复,架构问题作为中长期优化方向
  3. 发现问题并知道怎么改进,比假装没有问题更有价值
  4. 这些经验让我在未来的项目中,从第一天就会建立安全规范、测试覆盖和数据库迁移机制