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 | 评论内容 |
| platform | 0=Unknown/1=Android/2=iOS/3=Web/4=MiniApp |
| reviewStatus | NO_REVIEW / REVIEWING / APPROVED / REJECTED |
| rewardStatus | NOT_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 | 描述 |
| type | FOLDER / FILE |
| size | 文件大小 |
| parent_id | 父节点 ID(Tree 结构) |
| createdBy | 创建者 |
| deletedAt / deletedBy | 软删除审计 |
核心功能:
- 创建嵌套文件夹层级
- 批量文件上传(含重复名称检测)
- 替换文件(保持文件扩展名一致性)
- 级联软删除:删除文件夹时递归查找所有后代节点一并删除
- 分页列表支持关键词搜索、多字段排序(NAME / CREATED_AT / UPDATED_AT / SIZE)
2.6 配置管理模块
双模式配置管理:
- JSON 配置(SettingJsonAdmin):将配置文件存储在 OSS 上,通过管理端读写
- 通用配置(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({ 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 自定义错误码
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 | 记录方式 | 典型内容 |
|---|---|---|---|
| Error | nest-error | AllExceptionsFilter 自动 | 异常堆栈、请求上下文、traceId |
| Access | nest-access | ResponseInterceptor 自动 | 请求方法/路径/耗时/状态码/用户信息 |
| Business | nest-business | 手动调用 slsLogger | 业务操作(审批、积分发放等) |
| Debug | nest-debug | 手动调用 slsLogger | 调试数据、函数追踪 |
| Performance | nest-performance | PerformanceTracker 自动 | 各阶段耗时(db_query/external_api 等) |
- 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 性能埋点规范
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/
├── common.config.ts # 公共配置(端口 16666、默认 TTL 60s)
├── development.config.ts # 开发环境(synchronize: true)
├── test.config.ts # 测试环境
└── production.config.ts # 生产环境(CDN 刷新、SLS 启用)
九、技术方案深入剖析(面试细节)
9.1 @Auth 装饰器:组合式守卫链的设计
认证不是单一 Guard,而是通过 applyDecorators 组合多个装饰器形成守卫链:
// 装饰器工厂:根据 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,前者失败直接短路。
认证结果注入请求上下文:
// 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 评分模块:自动审核策略的业务逻辑
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 星评价 → 审核 → 积分奖励"的正向激励闭环
积分发放的跨服务调用:
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 服务:
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 表中存储节点路径:
数据示例:
| 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. |
级联软删除的实现:
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 的细节
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 场景使用:
@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 响应拦截器:非阻塞异步日志
// 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,但不会重试
})
);
用日志可靠性换响应速度——极端情况下日志可能丢失(SLS 写入失败),但响应延迟不受影响。对于非关键日志(Access/Performance),这是合理的取舍。
十、改进建议(面试加分项)
10.1 积分发放的事务一致性
现状:systemTrade() 和 save() 不在同一事务中,存在部分失败风险。
改进方案 A — 本地事务表:
// 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,导致多次认证调用。
改进方案:
// 两级缓存: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 避免级联故障 |
十一、面试亮点总结
- 双层认证 + 权限体系:
applyDecorators组合式 Guard 链,App 级 → User/Admin 级 → Permission 级,缓存外部认证减少跨服务调用 - 评分自动审核策略:1-4 星自动放行 + 5 星人工审核,非对称策略平衡运营效率与反刷评
- 积分奖励闭环:
bizOrderId幂等保证 + 跨服务 systemTrade 调用,可讨论事务一致性改进方案 - N+1 批量化:收集用户 ID → 单次批量查询,O(N) → O(1) 网络调用
- Materialized Path 树:TypeORM Tree Entity + 递归
findDescendants级联软删除 - 分布式锁:Redis SET NX EX 防止多实例 Cron 重复执行,TTL 兜底防死锁
- 非阻塞日志:ResponseInterceptor 中
tap()+Promise.allfire-and-forget,不影响响应延迟 - 5 级可观测性:Error/Access/Business/Debug/Performance + TraceId 全链路,安全序列化处理循环引用
- OSS + CDN 联动:文件上传 → OSS 存储 → CDN 缓存刷新,生产环境自动化
- 改进空间:积分发放可引入本地事务表保证一致性、统计查询应推到数据库层、认证缓存可用两级结构优化