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 分类 | 付费级别 |
|---|---|---|---|
| Base | AI_FAMOUS / AI_THEME / AI_EMOJI | Famous / Theme / Emoji | 免费 |
| Sex | AI_SLIDE_CONTENT / AI_VIP_CONTENT_VIDEO | Slideshow / Template | VIP |
| Adult | AI_SLIDE_ADULT / AI_VIP_ADULT_VIDEO | Slideshow / Template | VIP |
| Undress | AI_SLIDE_IMAGE / AI_VIP_IMAGE | Slideshow / Template | VIP |
通用功能:
- 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+ImageAPI 加载图片获取宽高,计算实际宽高比与 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 |
| HTTP | Axios(封装 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 解耦:
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)临时凭证管理机制:
// 核心设计: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) 的勾选/取消操作:
// Checkbox 矩阵:每个素材可以同时勾选多种生成类型
// 例如:素材 A 勾选了 "生成视频" 和 "生成 nude 视频"
selectedMaterials = {
101: { materialId: 101, taskTypes: ['VIP_FACE_SWAP', 'VIP_VIDEO_CLOTHES_CHANGE'] },
102: { materialId: 102, taskTypes: ['VIP_FACE_SWAP'] },
}
隐藏图片的 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 轴:
// 关键配置:两个 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 项指标的动态格式化
// 每个指标需要不同的格式化方式
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 配置不同。
改进方案:提取为配置驱动的通用组件:
// 一个配置 = 一个页面
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 调用,状态间存在隐式依赖(如关闭抽屉时需要重置多个状态)。
改进方案:
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 上传失败没有重试机制
改进方案:
// 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 类型定义 |