跳到主要内容

SWAI-管理后台

一、模块概览

SWAI 管理后台基于 UmiJS + Ant Design Pro + TypeScript 技术栈构建,负责 AI 换脸应用的运营数据看板、模板素材管理、AI 内容生产、用户反馈与评分审核等全链路运营管理能力。

架构设计

管理后台同时对接 Spring Boot(Java)、NestJS(Node)、Event Server 三个后端服务,通过统一的 request 工具和不同的 baseURL 实现透明路由,前端无需关心具体服务归属。


二、功能模块详解

2.1 数据看板(/swai/data)

三 Tab 架构的 BI 数据分析面板,涵盖用户增长、付费转化、留存分析全维度。

数据看板技术亮点
  • 双日期模式:支持任意多选日期与连续区间两种模式,7 种预设快捷选项
  • 22 项核心指标:覆盖用户、内容生成、付费、留存四大维度
  • Excel 导出:动态文件名含日期区间,支持四种格式化(千分位、时长、百分比、金额)
  • DualAxes 双轴图:柱状图 + 折线图组合,解决数量级差异的展示问题

2.2 素材模板管理(Base / Sex / Adult / Undress)

四个页面共享统一的 CRUD 管理模式,按内容类型和付费等级划分:

页面素材类型Tab 分类付费级别
BaseAI_FAMOUS / AI_THEME / AI_EMOJIFamous / Theme / Emoji免费
SexAI_SLIDE_CONTENT / AI_VIP_CONTENT_VIDEOSlideshow / TemplateVIP
AdultAI_SLIDE_ADULT / AI_VIP_ADULT_VIDEOSlideshow / TemplateVIP
UndressAI_SLIDE_IMAGE / AI_VIP_IMAGESlideshow / TemplateVIP

通用功能:

  • ProTable 分页列表 + 按名称搜索
  • ModalForm 创建/编辑弹窗
  • 阿里云 OSS 多文件上传(最多 10 个,picture-card 预览)
  • 上线/下线状态切换(Popconfirm 确认)
  • 权重排序(weight 字段控制展示顺序)
  • 积分消耗配置(unitCost / totalCost)

VIP 模板额外功能:

  • 跳转类型配置:通过 ProFormDependency 实现条件渲染,根据 mediaType(IMAGE/VIDEO)动态显示不同跳转类型选项
    • 图片类跳转:Photo Undress / Video Undress / Change Clothes
    • 视频类跳转:Photo Undress / Video Undress / Make Sex Video / Make Adult Video
  • 图片/视频混合预览:基于文件扩展名自动检测媒体类型,Image.PreviewGroup 预览图片,VideoPlayer 组件预览视频

2.3 女神管理(/swai/goddess)⭐ 最复杂模块

复杂度警告

这是整个管理后台最复杂的页面(1592 行、16+ 个 useState),包含主表格 → 抽屉 → 嵌套表格 → 弹窗共 3 层嵌套交互。建议面试时重点聊此模块的状态管理挑战和改进思路。

整个管理后台最复杂的页面(1100+ 行),实现了 AI 人物形象 → 模板选择 → 内容生产 → 结果管理 的完整工作流。

技术亮点:

  • 图片 3:4 比例验证:客户端使用 FileReader + Image API 加载图片获取宽高,计算实际宽高比与 3:4 的偏差是否在 2% 容忍度内
  • 多层嵌套交互:主表格 → 抽屉 → 嵌套表格 → 弹窗,最深 3 层嵌套
  • 异步任务流:提交生成任务 → 状态轮询(PENDING → PROCESSING → SUCCESS/FAILURE)→ 失败重试
  • 任务类型映射:通过 TASK_TYPE_MAP 将素材类型映射到具体生成类型(VIP_FACE_SWAP / VIP_VIDEO_CLOTHES_CHANGE 等)

2.4 反馈管理(/swai/feedback)

用户反馈的审核工作流,支持 Bug 报告和功能建议两种类型。

功能特点:

  • 多维度搜索:ID、类型(issue/suggestion)、状态、描述模糊搜索、taskId、用户 ID、时间范围
  • 截图/录屏混合预览:自动识别视频格式(.mp4/.webm/.ogg/.mov/.avi),分别使用 Image.PreviewGroup 和 VideoPlayer 渲染
  • 用户信息弹窗:10 字段 2 列 Descriptions 布局(头像、昵称、邮箱、性别、国家、账户类型等)
  • 关联生成内容查看:通过 taskId 调用 batchQueryGenerations 查看用户反馈的具体生成结果

2.5 商店评分管理(/swai/rating)

应用商店评分的审核与激励运营。

功能特点:

  • 统计看板:页面顶部展示总评分数、待审核数、已通过/拒绝数、积分发放状态
  • 审批流程:仅 reviewing 状态显示操作按钮;通过可配置积分奖励(建议 20-100 积分)
  • 重复提交检测:审批前查询用户历史评分记录,展示重复提交预警
  • 平台标识:Android / iOS / Web / MiniApp 四平台 Badge 显示

三、技术栈总览

类别技术选型
框架React 18 + TypeScript 5 + UmiJS 4
UI 库Ant Design 5 + @ant-design/pro-components 2
图表@ant-design/plots(Column / DualAxes / Pie)
样式TailwindCSS 3 + Less
HTTPAxios(封装 request 工具)
文件存储阿里云 OSS(ali-oss SDK)
Excel 导出xlsx 库
日期处理dayjs
API 类型OpenAPI/Swagger 自动生成

四、架构亮点

4.1 多后端服务集成架构

管理后台同时对接三个后端服务,通过统一的 request 工具和不同的 baseURL 实现透明路由:

服务路径前缀负责模块
Spring Boot (Java)/x/face-ai/, /x/face-goddess/素材管理、女神管理、内容生成
NestJS (Node)/node/feedback/, /node/rating/反馈管理、评分管理
Event Server/event/admin/数据看板

4.2 ProTable 驱动的配置化开发

所有列表页面基于 ProTable 的 columns 配置 + request 函数模式开发,将分页、搜索、排序逻辑与 UI 解耦:

ProTable request 适配模式
request={async (params) => {
const response = await apiCall({
pageNum: params.current || 1,
pageSize: params.pageSize || 10,
...filterParams
});
return { data: response.data?.list || [], total: response.data?.total || 0 };
}}

4.3 组件复用与模式一致性

  • 四个素材管理页面共享完全一致的 CRUD 交互模式,仅 materialType 枚举值不同
  • 统一的 actionRef 刷新机制:所有 CRUD 操作完成后通过 actionRef.current?.reload() 刷新表格
  • 统一的 OSS 上传工具uploadFilesToOss(EAppName.SWAI, files) 封装上传流程
  • 统一的媒体预览组件CustomImage(带 loading spinner)和 VideoPlayer(弹窗播放)

4.4 多应用架构支持

通过 EAppId 枚举实现多应用数据隔离,所有 API 调用携带 appId 参数,后台管理系统可无缝扩展支持新应用。


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

5.1 OSS 上传方案:STS 临时凭证 + 客户端缓存

安全最佳实践

使用 STS 临时凭证而非 AK/SK 直传,前端不暴露永久密钥;同时通过 Map 缓存 OSS Client 复用凭证,并在过期前 5 分钟自动清除,兼顾安全性与性能。

上传流程不是简单的前端直传,而是实现了一套STS(Security Token Service)临时凭证管理机制

utils/ossUpload.ts
// 核心设计:Map 缓存 OSS Client,避免重复申请 STS Token
const ossClientMap = new Map<string, OSS>();
const ossInfoMap = new Map<string, API.STSInfo>();

// 1. 首次上传 → 从后端获取 STS 临时凭证
const { data: ossInfo } = await stsInfo1({ appName });

// 2. 创建 OSS Client 并缓存
ossClientMap.set(appName, ossClient);

// 3. 设置定时器:Token 过期前 5 分钟自动清除缓存
setTimeout(() => {
ossClientMap.delete(appName);
ossInfoMap.delete(appName);
}, expireTime - Date.now() - 5 * 60 * 1000);

文件命名策略:MD5(文件名 + 大小 + 修改时间 + 相对路径 + 随机数) 生成唯一文件名,既避免冲突又支持 CDN 缓存。

面试可聊的点

  • 为什么用 STS 而不是 AK/SK 直传?→ 安全性,临时凭证有过期时间,前端不暴露永久密钥
  • 为什么缓存 OSS Client?→ STS 申请有频率限制,同一上传会话复用凭证
  • 5 分钟提前清除的设计意义?→ 避免使用即将过期的 Token 导致上传失败

5.2 女神页面状态管理:复杂度分析

Goddess.tsx 是 1592 行的巨型组件,使用了 16+ 个 useState 管理状态:

主表格状态: editModalOpen, editingRecord, actionRef
任务创建抽屉: taskDrawerOpen, currentGoddess, selectedImage, activeMaterialType, selectedMaterials, submitting
内容查看抽屉: contentDrawerOpen, currentGoddessForContent
素材详情弹窗: materialDetailModalOpen, materialDetail

素材选择算法 — 使用 Record<materialId, { taskTypes: string[] }> 结构,实现 O(1) 的勾选/取消操作:

Goddess.tsx - 素材选择数据结构
// Checkbox 矩阵:每个素材可以同时勾选多种生成类型
// 例如:素材 A 勾选了 "生成视频" 和 "生成 nude 视频"
selectedMaterials = {
101: { materialId: 101, taskTypes: ['VIP_FACE_SWAP', 'VIP_VIDEO_CLOTHES_CHANGE'] },
102: { materialId: 102, taskTypes: ['VIP_FACE_SWAP'] },
}

隐藏图片的 PreviewGroup 技巧

Goddess.tsx - 隐藏图片的 PreviewGroup 技巧
// 问题:只显示 4 张缩略图,但 PreviewGroup 需要能左右翻页所有图片
// 解决:在 DOM 中渲染隐藏的 Image 组件,加入 PreviewGroup
{images.slice(4).map(url => (
<CustomImage key={url} className="hidden" src={url} />
))}
// 点击 "+N" 时触发第 5 张隐藏图片的点击事件
<div onClick={() => document.querySelector('.hidden-img-5')?.click()}>
+{images.length - 4}
</div>

5.3 数据看板:DualAxes 双轴图数据转换

注册趋势图需要在同一图表中展示柱状图(安装/注册/活跃数)折线图(转化率%),两者数据量级差异巨大(千级 vs 百分比),必须使用双 Y 轴:

DataDashboard.tsx - DualAxes 双轴图配置
// 关键配置:两个 children 共享 xField 但使用独立的 yField 和 scale
children: [
{
data: columnData, // [{time, value, type:'安装'}, {time, value, type:'注册'}]
type: 'interval', // 柱状图
yField: 'value',
colorField: 'type',
group: true, // 分组柱状图
},
{
data: lineData, // [{time, value, type:'转化率'}]
type: 'line', // 折线图
yField: 'value',
axis: { y: { position: 'right', title: '转化率 (%)' } }, // 右侧 Y 轴
scale: { series: { independent: true } }, // 关键:独立 scale 避免柱状图被压缩
},
]

5.4 Excel 导出:22 项指标的动态格式化

utils/excelExport.ts
// 每个指标需要不同的格式化方式
const formatMap = {
activeUsersCount: formatNumber, // 1234 → "1,234"
rechargeAmount: formatAmount, // 12.5 → "$12.50"
conversionRate: formatPercentage, // 0.156 → "15.60%"
avgImageGenTime: formatDuration, // 125000 → "2m 5s"
};

// 导出时动态文件名包含日期范围
const fileName = `每日数据_${startDate}_${endDate}.xlsx`;

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

6.1 素材页面抽象:2800 行 → ~400 行

现状:Base/Sex/Adult/Undress 四个页面存在大量代码重复(~700 行/页),仅 materialType 枚举值和 Tab 配置不同。

改进方案:提取为配置驱动的通用组件:

components/MaterialPage/config.ts
// 一个配置 = 一个页面
interface MaterialPageConfig {
tabs: Array<{ key: string; label: string }>;
hasJumpTypes?: boolean; // 是否显示跳转类型
jumpTypeOptions?: string[]; // 跳转选项列表
}

const BaseConfig: MaterialPageConfig = {
tabs: [
{ key: 'AI_FAMOUS', label: '名人' },
{ key: 'AI_THEME', label: '主题' },
{ key: 'AI_EMOJI', label: 'Emoji' },
],
hasJumpTypes: false,
};

// 4 个页面共用一个 MaterialPage 组件
export default () => <MaterialPage config={BaseConfig} />;
重构收益

代码量减少 85%(2800 行 → ~400 行),Bug 修复一处生效四处,新增素材类型只需加配置,是面试中展示架构思维的好案例。

6.2 女神页面状态重构:useState → useReducer

现状:16 个独立的 useState 调用,状态间存在隐式依赖(如关闭抽屉时需要重置多个状态)。

改进方案

Goddess.tsx - useReducer 重构方案
type GoddessAction =
| { type: 'OPEN_TASK_DRAWER'; goddess: GoddessProfile }
| { type: 'SELECT_IMAGE'; url: string }
| { type: 'TOGGLE_MATERIAL'; materialId: number; taskType: string }
| { type: 'CLOSE_ALL_DRAWERS' } // 一个 action 重置所有相关状态

function goddessReducer(state: GoddessState, action: GoddessAction) {
switch (action.type) {
case 'CLOSE_ALL_DRAWERS':
return {
...state,
taskDrawer: { open: false },
contentDrawer: { open: false },
selectedMaterials: {}, // 同时清理
};
}
}

收益:状态转换可预测、可调试(配合 React DevTools),避免遗漏状态重置。

6.3 图片上传增强

已知问题

大图片可能导致 FileReader 内存溢出;损坏图片的 onload 可能永远不触发导致流程卡死;OSS 上传失败没有重试机制。以下改进方案针对这些问题逐一解决。

现有问题

  • 大图片可能导致 FileReader 内存溢出
  • 没有超时保护(损坏图片的 onload 可能永远不触发)
  • OSS 上传失败没有重试机制

改进方案

utils/uploadEnhanced.ts
// 1. 添加文件大小预检
if (file.size > 50 * 1024 * 1024) return reject('文件超过 50MB');

// 2. 添加 onload 超时保护
const timer = setTimeout(() => reject('图片加载超时'), 5000);
img.onload = () => { clearTimeout(timer); resolve(true); };
img.onerror = () => { clearTimeout(timer); reject('图片损坏'); };

// 3. OSS 上传添加指数退避重试
async function uploadWithRetry(file, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try { return await ossClient.put(key, file); }
catch { await sleep(Math.pow(2, i) * 1000); }
}
}

6.4 其他可讨论的改进方向

方向现状改进建议
错误边界页面级无 ErrorBoundary为每个 Tab/Drawer 添加局部错误边界,避免单组件崩溃影响全页
图片懒加载CustomImage 无 lazy loading列表图片添加 loading="lazy" 属性
虚拟滚动ProTable 全量渲染大数据量场景使用 @visactor/vtable 替代
图表 memo数据变更时全量重渲染React.memo 包装图表组件,避免不必要的重绘
类型安全部分 as any 类型断言补充完整的 TypeScript 类型定义