跳到主要内容

SWAI-NestJS后端服务

一、服务概览

Nest Server 是 SWAI 应用的核心 BFF(Backend For Frontend)服务,基于 NestJS + MySQL + Redis 构建,承担用户侧业务逻辑、管理侧审核运营、AI 对话、文件管理、定时任务等职责。与 Java 微服务(Spring Boot)协同工作,作为 Node.js 层的业务中台。


二、模块架构详解

2.1 AI 智能对话模块

API: POST /node/ai/user/get_answer(用户认证)

接入字节跳动豆包大模型(doubao-seed-1-6-flash-250615),支持多轮对话和多模态输入。

功能特点:

  • 多轮对话:支持 messages 数组传入历史上下文
  • 多模态输入:同时支持文本和图片(URL / Base64)
  • System Prompt:可配置系统角色指令
  • 流式/非流式:对接 Doubao Chat Completions API

2.2 商店评分模块 ⭐

数据模型 (store_rating):

字段说明
id (UUID)主键
appId应用 ID
createdBy评分用户 ID
rating (1-5)评分值
content评论内容
platform0=Unknown/1=Android/2=iOS/3=Web/4=MiniApp
reviewStatusNO_REVIEW / REVIEWING / APPROVED / REJECTED
rewardStatusNOT_ISSUED / ISSUED
pointsReward奖励积分数

API 端点:

  • 用户端:POST /node/rating/user/submit — 提交评分
  • 管理端:POST /node/rating/admin/get_list — 分页列表(含用户信息聚合)
  • 管理端:POST /node/rating/admin/approve — 批准 + 积分奖励
  • 管理端:POST /node/rating/admin/reject — 拒绝
  • 管理端:POST /node/rating/admin/statistics — 按星级统计
  • 管理端:POST /node/rating/admin/get_user_statistics — 用户历史评分统计
核心业务策略:非对称审核
  • 1-4 星自动放行(NO_REVIEW),无需人工介入,降低运营成本
  • 5 星进入人工审核(REVIEWING),确保高分评价质量,防止刷评
  • 审核通过后自动发放积分奖励,形成正向激励闭环

技术亮点:

  • 自动审核逻辑:1-4 星自动放行(NO_REVIEW),5 星进入人工审核(REVIEWING),降低运营成本的同时确保高分评价质量
  • 积分奖励集成:通过外部 Java 交易服务的 systemTrade() API 发放虚拟货币,每个应用有独立的 virtualCurrencyId
  • 用户信息聚合:Admin 列表接口通过批量调用 Java 用户服务获取用户详情,一次请求完成数据拼装

2.3 反馈管理模块

数据模型 (feedback):

  • 类型:ISSUE(Bug 报告)/ SUGGESTION(功能建议)
  • 附件:screenshots JSON 数组(支持图片/视频 URL)
  • 关联:taskId 可关联具体的 AI 生成内容
  • 搜索:description 和 reply 支持模糊匹配

API 端点:

  • 用户端:POST /node/feedback/user/create — 提交反馈
  • 用户端:GET /node/feedback/user/get_list — 近 30 天历史
  • 管理端:POST /node/feedback/admin/get_list — 多维度筛选列表
  • 管理端:POST /node/feedback/admin/accept|reject|process — 审核操作

2.4 文件管理模块

功能特点:

  • 单文件上传(POST /node/file/admin/upload)和批量上传(POST /node/file/admin/upload_batch
  • 文件校验:最大 1GB,MIME 类型白名单(image/video)
  • OSS 上传后自动刷新 CDN 缓存(生产环境)
  • 自定义 CDN 域名映射
  • 工具端点:POST /node/file/open/get_repaired_json — 修复格式错误的 JSON(基于 jsonrepair 库)

2.5 素材库模块 ⭐

基于 TypeORM Tree Entity 实现的层级文件管理系统。

数据模型 (material_library):

字段说明
id (UUID)主键
name文件/文件夹名
description描述
typeFOLDER / FILE
size文件大小
parent_id父节点 ID(Tree 结构)
createdBy创建者
deletedAt / deletedBy软删除审计

核心功能:

  • 创建嵌套文件夹层级
  • 批量文件上传(含重复名称检测)
  • 替换文件(保持文件扩展名一致性)
  • 级联软删除:删除文件夹时递归查找所有后代节点一并删除
  • 分页列表支持关键词搜索、多字段排序(NAME / CREATED_AT / UPDATED_AT / SIZE)

2.6 配置管理模块

双模式配置管理:

  1. JSON 配置(SettingJsonAdmin):将配置文件存储在 OSS 上,通过管理端读写
  2. 通用配置(SettingGeneral):存储在数据库中,支持版本管理和回滚

通用配置特点:

  • 每次修改自动递增版本号
  • 保留完整版本历史
  • 支持回滚到任意历史版本
  • 公开读取接口缓存 180 秒

2.7 定时任务模块 ⭐

分布式定时任务关键设计
  • Redis 分布式锁:通过 SET key NX EX 原子操作防止多实例重复执行
  • 环比数据分析:对比当前小时与上一小时数据,自动计算百分比变化并标注颜色
  • 飞书卡片推送:使用卡片模板格式化数据报表,自动推送到运营群

2.8 健康模块

血压记录功能:

  • GET /node/health/blood/user/get_blood_pressure_records — 按日期范围查询
  • POST /node/health/blood/user/create_blood_pressure_record — 创建记录
  • 数据隔离:每个用户只能查看自己的记录

三、认证鉴权体系 ⭐

3.1 双层认证架构

使用方式:

auth.decorator.ts
@Auth({ type: 'user' })              // C端用户认证
@Auth({ type: 'admin' }) // B端管理认证(无权限要求)
@Auth({ type: 'admin', permissionId: 'system:rating' }) // B端 + 特定权限

权限列表:

  • system:rating — 评分管理
  • system:feedback — 反馈管理
  • system:material — 素材库管理
  • system:setting:general — 通用配置管理
  • system:setting:json — JSON 配置管理

3.2 认证缓存策略

外部 Java 认证服务的 Token 验证结果缓存 60 秒,减少跨服务调用频率。缓存 Key 包含 appId 和 token,确保多应用数据隔离。


四、错误处理体系

4.1 自定义错误码

error-codes.ts
10001 - PARAMS_ERROR       参数校验失败
10002 - NOT_FOUND 资源未找到
10003 - UNAUTHORIZED 未认证
10004 - FORBIDDEN 无权限
10005 - SERVER_ERROR 服务器内部错误
10006 - DATABASE_ERROR 数据库错误
10007 - REPEAT_OPERATION 重复操作
10008 - INVALID_OPERATION 无效操作
10009~10023 - 各类细分校验错误

4.2 统一响应格式

成功响应:

成功响应示例
{
"code": 200,
"msg": "success",
"data": { ... },
"path": "/node/rating/user/submit",
"date": "2024-12-25T10:00:00.000Z",
"traceId": "uuid-xxx"
}

分页响应:

分页响应示例
{
"data": {
"list": [...],
"total": 100,
"pageNum": 1,
"pageSize": 10,
"pages": 10
}
}

五、可观测性体系(5 级 SLS 日志)

与 Event Server 共享同一套日志架构:

日志级别Logstore记录方式典型内容
Errornest-errorAllExceptionsFilter 自动异常堆栈、请求上下文、traceId
Accessnest-accessResponseInterceptor 自动请求方法/路径/耗时/状态码/用户信息
Businessnest-business手动调用 slsLogger业务操作(审批、积分发放等)
Debugnest-debug手动调用 slsLogger调试数据、函数追踪
Performancenest-performancePerformanceTracker 自动各阶段耗时(db_query/external_api 等)
TraceId 全链路追踪
  • TraceMiddleware 为每个请求生成 UUID traceId
  • traceId 写入响应 Header(X-Trace-Id)
  • 所有日志记录携带 traceId,实现全链路关联

六、缓存架构


七、技术栈总览

类别技术选型
框架NestJS 10 + TypeScript 5
数据库MySQL 5.7+ + TypeORM 0.3(Tree Entity)
缓存Redis + CacheableMemory(两级缓存)
文件存储阿里云 OSS + CDN 自动刷新
AI 对话字节豆包 Doubao API(doubao-seed-1-6-flash)
日志阿里云 SLS(5 级日志)
定时任务@nestjs/schedule + Redis 分布式锁
限流@nestjs/throttler(60 req/60s)
文档Swagger/OpenAPI 自动生成
消息推送飞书 Webhook(卡片模板)
部署Docker + Kubernetes
API 类型OpenAPI 代码生成(Java 服务客户端)

八、软件工程实践

8.1 软删除审计

所有业务实体使用 @DeleteDateColumn + deletedBy 字段:

  • TypeORM 查询自动过滤已删除记录
  • 保留删除操作人和时间的完整审计轨迹
  • 支持数据恢复

8.2 性能埋点规范

performance-tracker 命名规范
db_query_xxx / db_create_xxx / db_update_xxx / db_delete_xxx  → 数据库操作
external_api_xxx → 外部服务调用
data_transform_xxx / calculate_xxx → 数据处理
auth_xxx_verify / auth_permission_check → 认证鉴权

8.3 多环境配置

config/
config/
├── common.config.ts # 公共配置(端口 16666、默认 TTL 60s)
├── development.config.ts # 开发环境(synchronize: true)
├── test.config.ts # 测试环境
└── production.config.ts # 生产环境(CDN 刷新、SLS 启用)


九、技术方案深入剖析(面试细节)

9.1 @Auth 装饰器:组合式守卫链的设计

认证不是单一 Guard,而是通过 applyDecorators 组合多个装饰器形成守卫链:

auth/auth.decorator.ts
// 装饰器工厂:根据 type 组合不同的 Guard 栈
export function Auth(params: TAuthParams) {
const guards = [AuthAppGuard]; // 始终包含 App 级验证

if (params.type === 'user') {
guards.push(AuthUserGuard);
} else if (params.type === 'admin') {
guards.push(AuthAdminGuard);
}

return applyDecorators(
SetMetadata('permissionId', params.permissionId), // 注入权限标识
UseGuards(...guards), // 组合守卫
);
}

Guard 执行顺序:AuthAppGuard → AuthUserGuard/AuthAdminGuard,前者失败直接短路。

认证结果注入请求上下文

auth/guards/*.guard.ts
// Guard 验证通过后,用户信息挂载到 request 对象
request.userBaseInfo = { id: uid, appId, platform }; // C 端用户
request.adminUserInfo = { uid, loginName, pemimList }; // B 端管理员

// Service 层通过 @Req() 获取
async approve(@Req() request: IRequest) {
const adminId = request.adminUserInfo.uid;
}

面试可聊的点

  • 为什么不用一个通用 Guard 加 if/else?→ 职责分离、可独立测试、可自由组合
  • SetMetadata + Reflector 的设计模式?→ NestJS 的元编程能力,装饰器注入元数据 → Guard 通过 Reflector 读取
  • 缓存 Key 为什么包含 appId?→ 同一 Token 在不同应用中可能对应不同权限

9.2 评分模块:自动审核策略的业务逻辑

rating/rating.service.ts
async submitRating(dto: RatingSubmitReqDto, request: IRequest) {
const rating = new RatingEntity();
rating.rating = dto.rating;
rating.createdBy = request.userBaseInfo.id;

// 核心策略:非对称审核
if (dto.rating === 5) {
rating.reviewStatus = EReviewStatus.REVIEWING; // 5 星 → 人工审核
} else {
rating.reviewStatus = EReviewStatus.NO_REVIEW; // 1-4 星 → 自动通过
}
rating.rewardStatus = ERewardStatus.NOT_ISSUED; // 初始未发放

return this.ratingRepository.save(rating);
}

为什么只审核 5 星?

  • 5 星评价是应用商店排名的关键因素,容易成为刷评目标
  • 1-4 星无激励价值,无需消耗运营人力审核
  • 审核通过后发放积分奖励,形成"5 星评价 → 审核 → 积分奖励"的正向激励闭环

积分发放的跨服务调用

rating/rating.service.ts
async approve(dto: RatingApproveReqDto, request: IRequest) {
const rating = await this.ratingRepository.findOne({ where: { id: dto.id } });

// 1. 调用 Java 交易服务发放虚拟货币
if (dto.pointsReward && dto.pointsReward > 0) {
await systemTrade({
uid: rating.createdBy,
appId: rating.appId,
bizOrderId: rating.id, // 幂等标识:用 rating ID 防止重复发放
amount: dto.pointsReward,
virtualCurrencyId: APP_CURRENCY_MAP[rating.appId],
}, { headers: { authorization: authToken } });
}

// 2. 更新评分状态
rating.reviewStatus = EReviewStatus.APPROVED;
rating.rewardStatus = ERewardStatus.ISSUED;
rating.pointsReward = dto.pointsReward;
await this.ratingRepository.save(rating);
}
积分发放的一致性风险

systemTrade()save() 不在同一事务中。如果 systemTrade() 成功但 save() 失败,积分已发但状态未更新,导致数据不一致。改进方案见第十章。

面试可聊的点

  • bizOrderId: rating.id 实现了幂等性——同一 rating 多次调用交易 API 不会重复发放
  • 但存在潜在问题:如果 systemTrade() 成功但 save() 失败,积分已发但状态未更新(详见改进建议)

9.3 用户信息聚合:N+1 问题的批量化解决

管理端列表需要展示用户详情,但评分表只存了 createdBy(用户 ID),用户详情在外部 Java 服务:

rating/rating.service.ts
async getAdminList(dto: RatingAdminListReqDto, request: IRequest) {
// 1. 查询评分列表
const [list, total] = await this.ratingRepository.findAndCount({ ... });

// 2. 收集所有用户 ID → 批量查询(避免 N+1)
const userIds = list.map(item => item.createdBy);
const { data: userMap } = await adminBatchBaseInfo(
{ uids: userIds },
{ headers: { authorization: authToken } }
);
// userMap: { "123": { nickname, avatar, email, ... }, "456": { ... } }

// 3. O(1) 拼装
return list.map(item => ({
...item,
userInfo: userMap?.[item.createdBy] || {},
}));
}

对比 N+1 模式

  • N+1:1 次列表查询 + N 次用户查询 = 11 次网络请求(10 条数据)
  • 批量化:1 次列表查询 + 1 次批量用户查询 = 2 次网络请求

9.4 素材库:Materialized Path 树形结构

TypeORM 的 @Tree('materialized-path') 策略在 material_library 表中存储节点路径:

material_library 表数据示例
数据示例:
| id | name | type | mpath |
|-----|----------|--------|--------------|
| 1 | root | FOLDER | 1. |
| 2 | images | FOLDER | 1.2. |
| 3 | bg.png | FILE | 1.2.3. |
| 4 | logo.png | FILE | 1.2.4. |
| 5 | videos | FOLDER | 1.5. |

级联软删除的实现

material/material.service.ts
async deleteFiles(ids: string[], request: IRequest) {
// 1. 找到所有目标节点
const list = await this.libraryRepository.findByIds(ids);

// 2. 对每个节点查找其所有后代
const allDescendants = await Promise.all(
list.map(item =>
this.libraryRepository.manager
.getTreeRepository(LibraryEntity)
.findDescendants(item) // 利用 mpath LIKE 'x.%' 查找
)
);

// 3. 批量软删除(标记 deletedAt + deletedBy)
const flatList = allDescendants.flat();
flatList.forEach(item => {
item.deletedAt = new Date();
item.deletedBy = request.adminUserInfo?.loginName;
});
await this.libraryRepository.save(flatList);
}

面试可聊的点

  • Materialized Path 的优劣?→ 读取子树快(LIKE 查询),但移动节点需要更新所有后代的路径
  • 为什么不用 Closure Table?→ TypeORM 原生支持 materialized-path,开发成本低
  • 为什么软删除而非硬删除?→ 保留审计轨迹 + 支持误删恢复

9.5 分布式锁:Redis SET NX EX 的细节

utils/redis-lock.ts
async function tryRedisLock({ key, expire, cacheManager }): Promise<boolean> {
const redis = (cacheManager as any).store?.getClient?.(); // 获取底层 Redis 客户端

// SET key "locked" NX EX expire
// NX: 仅当 key 不存在时才设置(原子操作)
// EX: 设置过期时间,防止死锁
const result = await redis.set(key, 'locked', 'EX', expire, 'NX');

return result === 'OK'; // 'OK' = 获取成功, null = 已被占用
}

Cron 场景使用

task/task.service.ts
@Cron('0 0 * * * *')  // 每小时整点
async handleSwaiDataCron() {
if (config.environment !== 'production') return; // 仅生产环境

const locked = await tryRedisLock({
key: `${redisNamespace}:send:swai:data:cron:lock`,
expire: 300, // 5 分钟后自动释放
});

if (!locked) return; // 其他实例已在执行

// 执行数据聚合 + 飞书推送
await this.aggregateAndSend();
// 注意:没有显式释放锁,依赖 TTL 自动过期
}

面试可聊的点

  • 为什么不显式释放锁?→ 简化逻辑,TTL 足够覆盖任务执行时间
  • 5 分钟 TTL 的设计依据?→ 数据聚合 + 飞书推送通常 10-30 秒完成,5 分钟提供足够余量
  • 如果任务执行超过 5 分钟会怎样?→ 锁自动释放,另一实例可能重复执行——这是一个边界场景

9.6 响应拦截器:非阻塞异步日志

interceptors/response.interceptor.ts
// ResponseInterceptor 中日志不阻塞响应返回
return next.handle().pipe(
map(data => ({ code: 200, data, msg: 'success', traceId })), // 包装响应
tap(data => {
// Fire-and-forget:日志异步执行,不影响响应延迟
Promise.all([
slsLogger.logAccess({ request, statusCode, responseTime }),
slsLogger.logPerformanceByRequest({ request, statusCode }),
]).catch(err => console.error('Log failed:', err));
// ↑ catch 防止 unhandled rejection,但不会重试
})
);
设计权衡:可靠性 vs 性能

用日志可靠性换响应速度——极端情况下日志可能丢失(SLS 写入失败),但响应延迟不受影响。对于非关键日志(Access/Performance),这是合理的取舍。


十、改进建议(面试加分项)

10.1 积分发放的事务一致性

现状systemTrade()save() 不在同一事务中,存在部分失败风险。

改进方案 A — 本地事务表

rating/rating.service.ts (改进方案)
// 1. 先在本地数据库记录"待发放"
await queryRunner.startTransaction();
rating.reviewStatus = EReviewStatus.APPROVED;
rating.rewardStatus = ERewardStatus.PENDING; // 新增中间状态
await queryRunner.manager.save(rating);
await queryRunner.commitTransaction();

// 2. 异步调用交易 API
try {
await systemTrade({ ... });
rating.rewardStatus = ERewardStatus.ISSUED;
await this.ratingRepository.save(rating);
} catch {
// 记录失败,定时任务重试
await this.retryQueue.add({ ratingId: rating.id });
}

改进方案 B — 幂等重试:交易 API 已通过 bizOrderId 保证幂等,可以安全重试直到两边都成功。

10.2 评分统计的数据库下推

现状getStatistics() 加载所有评分到内存后在应用层聚合,大数据量会 OOM。

改进方案

优化后的统计查询
-- 将聚合推到数据库层
SELECT
rating,
COUNT(*) AS totalCount,
COUNT(DISTINCT created_by) AS uniqueUserCount
FROM store_rating
WHERE app_id = ? AND deleted_at IS NULL
GROUP BY rating;

一次 SQL 替代全量 getMany() + 内存聚合,内存占用从 O(N) 降为 O(1)。

10.3 认证缓存优化

现状:每个 Token 独立缓存,同一用户在不同设备有不同 Token,导致多次认证调用。

改进方案

auth/cache-strategy.ts
// 两级缓存:Token → userId(快) + userId → UserInfo(共享)
const tokenCacheKey = `nestjs:token:${appId}:${token}`;
const userId = await cache.get(tokenCacheKey);

if (userId) {
// 快速路径:直接从 userId 缓存获取用户信息
const userInfoKey = `nestjs:user:info:${appId}:${userId}`;
return await cache.get(userInfoKey);
}

// 慢路径:调用认证 API
const userInfo = await authenticate(token);
await cache.set(tokenCacheKey, userInfo.id, 60000);
await cache.set(`nestjs:user:info:${appId}:${userInfo.id}`, userInfo, 300000);

同一用户的多个 Token 共享用户信息缓存,减少外部 API 调用。

10.4 其他可讨论的改进方向

方向现状改进建议
AI API Key硬编码在代码中迁移到 ConfigService / 环境变量
AI 超时Doubao API 无超时设置添加 axios timeout + 用户侧超时提示
素材库移动不支持移动文件夹移动需更新所有后代的 mpath,或考虑 Closure Table
配置版本仅保留 content + lastContent改为版本历史表,支持任意版本回滚
飞书推送无重试、无超时添加 fetch timeout + 指数退避重试
外部 API 熔断认证/交易 API 无熔断保护添加 Circuit Breaker 避免级联故障

十一、面试亮点总结

  1. 双层认证 + 权限体系applyDecorators 组合式 Guard 链,App 级 → User/Admin 级 → Permission 级,缓存外部认证减少跨服务调用
  2. 评分自动审核策略:1-4 星自动放行 + 5 星人工审核,非对称策略平衡运营效率与反刷评
  3. 积分奖励闭环bizOrderId 幂等保证 + 跨服务 systemTrade 调用,可讨论事务一致性改进方案
  4. N+1 批量化:收集用户 ID → 单次批量查询,O(N) → O(1) 网络调用
  5. Materialized Path 树:TypeORM Tree Entity + 递归 findDescendants 级联软删除
  6. 分布式锁:Redis SET NX EX 防止多实例 Cron 重复执行,TTL 兜底防死锁
  7. 非阻塞日志:ResponseInterceptor 中 tap() + Promise.all fire-and-forget,不影响响应延迟
  8. 5 级可观测性:Error/Access/Business/Debug/Performance + TraceId 全链路,安全序列化处理循环引用
  9. OSS + CDN 联动:文件上传 → OSS 存储 → CDN 缓存刷新,生产环境自动化
  10. 改进空间:积分发放可引入本地事务表保证一致性、统计查询应推到数据库层、认证缓存可用两级结构优化