- Update yarn.lock - Add project implementation docs in docs/ - Add personal internship experience notes in 实习讲解/
389 lines
17 KiB
Markdown
389 lines
17 KiB
Markdown
# API 接口前后端集成 — 面试版
|
||
|
||
> 简历原话:**"协作设计可信态势感知平台API接口,完成50+数据接口的前后端集成"**
|
||
>
|
||
> 这篇文档帮你理解 API 集成层到底做了什么、怎么做的,以及面试时怎么讲。
|
||
|
||
---
|
||
|
||
## 一、先搞清楚:API 集成层是什么?
|
||
|
||
API 集成层是前端和后端之间的"翻译官"和"交通管制员"。它不是简单的 `axios.get('/xxx')`,而是一整套基础设施:
|
||
|
||
```
|
||
页面组件(Vue 组件)
|
||
│ 调用 API 函数
|
||
▼
|
||
API 函数层(29 个文件,641 个函数)
|
||
→ 定义接口 URL、method、参数格式
|
||
│ 调用 request()
|
||
▼
|
||
请求基础设施(request.ts,273 行)
|
||
→ Axios 实例、请求拦截器、响应拦截器
|
||
→ AES 加密/解密、Token 注入、Token 过期自动刷新
|
||
→ 错误统一处理、缓存降级
|
||
│ HTTP 请求
|
||
▼
|
||
后端服务器
|
||
```
|
||
|
||
**一句话概括**:我参与设计和集成了平台的 API 通信层,包括定义了 50+ 个态势感知相关的数据接口,并搭建了完整的请求基础设施(加密、Token 刷新、错误处理、缓存降级),覆盖全平台 641 个接口。
|
||
|
||
---
|
||
|
||
## 二、API 整体规模
|
||
|
||
| 数据 | 数值 |
|
||
|------|------|
|
||
| API 函数总数 | **641 个** |
|
||
| API 文件数 | **29 个** |
|
||
| 金银湖态势感知相关 | **106 个**(situation.ts 50 + index.ts 45 + home.ts 5 + auth.ts 3 + scanCenter.ts 3) |
|
||
| 最大的文件 | `repo/index.ts`(161 个函数) |
|
||
|
||
### 按模块分布
|
||
|
||
| 模块 | 文件 | 函数数 | 负责什么 |
|
||
|------|------|--------|---------|
|
||
| **态势感知看板** | `jyh/situation.ts` | 50 | 威胁地图、CVE 趋势、组件排行、漏洞分布等 |
|
||
| **金银湖核心** | `jyh/index.ts` | 45 | AI 对话、版本详情、SBOM、许可证、修复建议 |
|
||
| **仓库管理** | `repo/index.ts` | 161 | 仓库 CRUD、分支、标签、Wiki、文件、Webhook |
|
||
| **组织管理** | `org/index.ts` | 55 | 组织 CRUD、成员、首页、CLA、开发者门户 |
|
||
| **用户管理** | `user/index.ts` | 55+ | 登录注册、OAuth、个人资料、SSH 密钥、Token |
|
||
| **合并请求** | `merge/index.ts` | 62 | MR 创建/审查、差异对比、代码评审、门禁 |
|
||
| **讨论系统** | `discussion/index.ts` | 50 | 讨论 CRUD、分类、评论、投票、排行榜 |
|
||
| **其他 20+ 个文件** | issue、commit、branch 等 | ~120 | Issue、提交、分支、标签、发布、通知等 |
|
||
|
||
---
|
||
|
||
## 三、请求基础设施(面试核心 — request.ts)
|
||
|
||
`src/utils/request.ts`(273 行)是整个平台 API 通信的中枢,每一行都在解决实际问题。
|
||
|
||
### 3.1 整体架构
|
||
|
||
```
|
||
组件调用 API 函数
|
||
│
|
||
▼
|
||
proxyService(params) ← 创建一个 Axios 实例
|
||
│
|
||
├── 请求拦截器 #1:注入 Header
|
||
│ ├── DP_TOKEN / DP_REFRESH_TOKEN(Token 头)
|
||
│ ├── Authorization: Bearer xxx(权限头)
|
||
│ ├── page-title / page-repo-id / page-ref(页面监控头)
|
||
│ ├── gitcode-utm-source(来源追踪头)
|
||
│ └── URL 前缀重写(setPassportPrefix /uc)
|
||
│
|
||
├── 请求拦截器 #2:缓存降级检查
|
||
│ └── degradeInterceptor 决定是否读缓存
|
||
│
|
||
├── ===== 发出 HTTP 请求 =====
|
||
│
|
||
├── 响应拦截器 #1:成功处理
|
||
│ ├── code===500 → 检查登录状态
|
||
│ └── AES 解密响应数据($Decrypt)
|
||
│
|
||
├── 响应拦截器 #1:失败处理
|
||
│ ├── 401 → Token 自动刷新(refreshing 锁 + 请求队列)
|
||
│ ├── 其他 4xx/5xx → dealWarning(防抖 200ms)
|
||
│ └── 网络超时 → 静默处理
|
||
│
|
||
└── 响应拦截器 #2:缓存降级存储
|
||
└── 成功时存缓存,失败时读缓存返回
|
||
```
|
||
|
||
### 3.2 AES 加密解密
|
||
|
||
```typescript
|
||
// request.ts — 所有响应数据经过 AES-128-CBC 解密
|
||
import CryptoJS from 'crypto-js';
|
||
|
||
const sKey = CryptoJS.enc.Utf8.parse('mXzfCSPBKmEA8aLq');
|
||
const iv = CryptoJS.enc.Utf8.parse('tbeJJLC6dZQXXtWr');
|
||
|
||
// 解密:响应拦截器中自动调用
|
||
const $Decrypt = (text) => {
|
||
let src = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Base64.parse(text));
|
||
let bytes = CryptoJS.AES.decrypt(src, sKey, {
|
||
iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7
|
||
});
|
||
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
|
||
};
|
||
|
||
// 响应拦截器中:response.data.data = $Decrypt(response.data.data)
|
||
```
|
||
|
||
**为什么用加密?** 态势感知平台涉及敏感的安全数据,AES-128-CBC 加密保证了传输过程中即使被截获也无法直接读取。
|
||
|
||
### 3.3 Token 自动刷新 + 请求队列
|
||
|
||
这是整个基础设施中设计最巧妙的部分——**当 Token 过期时,多个并发请求只触发一次刷新**:
|
||
|
||
```typescript
|
||
// request.ts — Token 刷新逻辑(简化版)
|
||
let refreshing = false; // 刷新锁
|
||
const queue = []; // 请求等待队列
|
||
|
||
// 响应拦截器的错误处理分支
|
||
async (error) => {
|
||
if (response?.status === 401 && 有Token && 不是刷新接口) {
|
||
if (refreshing) {
|
||
// 情况 A:已经有刷新在进行 → 当前请求排队等待
|
||
return new Promise((resolve) => {
|
||
queue.unshift({ config, resolve });
|
||
});
|
||
}
|
||
|
||
// 情况 B:第一个遇到 401 的请求 → 执行刷新
|
||
refreshing = true;
|
||
const res = await refreshToken(); // 调刷新接口
|
||
refreshing = false;
|
||
|
||
if (res.status === 200) {
|
||
// 刷新成功 → 更新 Token → 重放所有排队的请求
|
||
localStorage.setItem('op_access_token', newToken);
|
||
queue.forEach(({ config, resolve }) => {
|
||
config.headers.Authorization = `Bearer ${newToken}`;
|
||
resolve(axios(config)); // 用新 Token 重发
|
||
});
|
||
queue.splice(0); // 清空队列
|
||
return axios(config); // 重发当前请求
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**场景**:用户打开一个有 5 个图表的页面,5 个请求同时发出,全部返回 401。第一个请求触发 `refreshToken`,其余 4 个进入队列等待。刷新成功后,5 个请求用新 Token 全部重发。**不会出现 5 个请求各自刷新一次 Token 的情况。**
|
||
|
||
### 3.4 错误统一处理(status.ts)
|
||
|
||
```typescript
|
||
// status.ts — HTTP 状态码 → 用户提示 + 事件通知
|
||
export function showMessage(res) {
|
||
switch (res.status) {
|
||
case 400: return res.data.error_message; // 参数错误
|
||
case 401: emitEvent('logout', ...); break; // 未授权 → 触发登出
|
||
case 403: emitEvent('forbiddenRefresh'); // 无权限 → 刷新页面
|
||
case 404: break; // 不存在 → 不提示
|
||
case 408: return '请求超时';
|
||
case 500: return '服务器错误';
|
||
case 502: return '网络错误';
|
||
case 504: return '网络超时';
|
||
default: return res.data.error_message || '连接出错';
|
||
}
|
||
}
|
||
|
||
// 用防抖包裹(200ms),防止短时间内多次弹错误提示
|
||
export function dealWarning() {
|
||
return debounce((res) => {
|
||
const msg = showMessage(res);
|
||
if (msg) Message.error(msg);
|
||
}, 200);
|
||
}
|
||
```
|
||
|
||
**面试话术**:
|
||
> "请求基础设施有四个关键设计。第一,AES-128-CBC 加密传输,敏感数据在传输过程中全程加密。第二,Token 自动刷新 + 请求队列——用 refreshing 锁和 Promise 队列机制,保证多个并发的 401 请求只触发一次 Token 刷新,其余排队等待,刷新成功后统一重发。第三,错误统一处理——所有 HTTP 错误状态码映射为用户友好的提示信息,并用 200ms 防抖避免短时间弹多次错误。第四,API 缓存降级——请求成功时缓存到 IndexedDB,失败时自动读缓存兜底。"
|
||
|
||
---
|
||
|
||
## 四、API 函数层 — 50+ 态势感知接口怎么组织的?
|
||
|
||
### 4.1 统一的 API 函数格式
|
||
|
||
每个 API 函数遵循统一的格式:
|
||
|
||
```typescript
|
||
// src/api/jyh/situation.ts — 示例:态势感知看板 API 函数
|
||
import request from '@/utils/request';
|
||
|
||
// 格式:export function 函数名(参数): 返回类型
|
||
// ↓ 语义化命名 ↓ TypeScript 泛型
|
||
|
||
// 全球威胁地图数据
|
||
export function mapDataPage1(params?: any): Promise<any> {
|
||
return request({
|
||
url: `/api/v1/page1/map/mapData`, // ← 接口路径
|
||
method: 'get', // ← 请求方法
|
||
params // ← GET 方式用 params
|
||
});
|
||
}
|
||
|
||
// AI 对话(POST 方式)
|
||
export function fetchChatUseSql(data): Promise<any> {
|
||
return request({
|
||
url: '/ai/v1/chat/use_sql', // ← AI 服务接口
|
||
method: 'POST', // ← POST 请求
|
||
data // ← POST 方式用 data 传 JSON body
|
||
});
|
||
}
|
||
|
||
// 文件上传(FormData 方式)
|
||
export function uploadImage(data): Promise<any> {
|
||
const formData = new FormData();
|
||
formData.append('file', data.file);
|
||
return request({
|
||
url: '/api/v1/upload/image',
|
||
method: 'POST',
|
||
data: formData,
|
||
headers: { 'Content-Type': 'multipart/form-data' }
|
||
});
|
||
}
|
||
```
|
||
|
||
### 4.2 态势感知相关的 50+ 接口
|
||
|
||
态势感知模块的接口集中在 `src/api/jyh/situation.ts`(50 个函数),按三个页面组织:
|
||
|
||
| 页面 | 接口前缀 | 端点数 | 干什么 |
|
||
|------|---------|--------|--------|
|
||
| **第1页:全球风险态势** | `/api/v1/page1/*` | 13 个 | 威胁地图、CVE 趋势、组件排行、漏洞分布、行业饼图 |
|
||
| **第2页:开源生态与贡献** | `/api/v1/page2/*` | ~18 个 | 企业榜单、基础设施覆盖率、高校俱乐部、社区健康、语言竞争力、开发者、热力图 |
|
||
| **第3页:武汉市开源生态** | `/api/v1/page3/*` | ~19 个 | HarmonyOS 统计、项目分布、开发者、企业、高校、社区、政策、镜像站点 |
|
||
|
||
加上 `jyh/index.ts` 中的 45 个通用接口(AI 对话、版本详情、SBOM、许可证、预警订阅、修复建议、质量任务等),金银湖模块共有 **106 个 API**。
|
||
|
||
---
|
||
|
||
## 五、两个错误处理工具函数
|
||
|
||
### 5.1 reqCatch — 安全包装 API 调用
|
||
|
||
```typescript
|
||
// src/utils/catch.ts
|
||
// 把 try/catch 包装成一个通用函数,避免每个组件都写 try/catch
|
||
export async function reqCatch(req, params) {
|
||
try {
|
||
const data = await req(params);
|
||
return { data, error: null }; // 成功 → 返回数据
|
||
} catch (e) {
|
||
return { data: null, error: e }; // 失败 → 返回错误对象,不会 throw
|
||
}
|
||
}
|
||
|
||
// 用法:
|
||
const res = await reqCatch(getOrg, { orgId: 'xxx' });
|
||
if (!res.error) {
|
||
// 成功处理
|
||
} else if (res.error.error_code === 404) {
|
||
// 404 处理
|
||
}
|
||
```
|
||
|
||
### 5.2 reqCatchV2 — 升级版,自动发事件
|
||
|
||
```typescript
|
||
export async function reqCatchV2(req) {
|
||
try {
|
||
const data = await req();
|
||
return { data, error: null };
|
||
} catch (e) {
|
||
emitEvent('responseError', e); // ← 自动通过事件总线通知全局
|
||
return { data: null, error: e };
|
||
}
|
||
}
|
||
```
|
||
|
||
**为什么有两版?** `reqCatch` 是早期版本,静默捕获错误让调用方自己处理。`reqCatchV2` 增加了自动事件通知——在路由守卫里用 V2,错误会被 `eventBus` 监听到,触发统一的 403/404 页面跳转。
|
||
|
||
---
|
||
|
||
## 六、API 缓存降级系统(degradeInterceptor.ts)
|
||
|
||
这是一个 360 行的自研系统,核心思想是:**API 成功时缓存结果,失败时读缓存兜底**。
|
||
|
||
```
|
||
请求发出
|
||
│
|
||
├── 请求拦截器
|
||
│ ├── 是否匹配缓存策略?(URL 正则匹配)
|
||
│ ├── 是否处于降级状态?(上次请求失败,且在熔断时间内)
|
||
│ │ → 是:直接读 IndexedDB 缓存返回(不发请求)
|
||
│ │ → 否:正常发请求(带重试增强)
|
||
│
|
||
├── 请求成功 → 响应拦截器
|
||
│ └── 缓存结果到 IndexedDB + 更新状态为 success
|
||
│
|
||
└── 请求失败 → 响应拦截器
|
||
├── 记录失败状态 + 时间戳
|
||
└── 读 IndexedDB 缓存返回(用户看到旧数据而不是错误)
|
||
```
|
||
|
||
**关键配置**(`storage-apis.ts`):
|
||
```typescript
|
||
const storageApis = [
|
||
{ url: '/api/v1/issues', timeout: 5000, retry: 2 },
|
||
{ url: '/api/v1/merge_requests', timeout: 5000, retry: 2 },
|
||
// ...
|
||
];
|
||
```
|
||
|
||
**面试话术**:
|
||
> "API 缓存降级系统的思路是'请求成功时缓存,失败时读缓存兜底'。在请求拦截器里判断是否命中缓存策略——如果上次请求失败且在熔断时间内,就直接读 IndexedDB 返回缓存数据,不发网络请求。响应成功时自动更新缓存,失败时记录状态并返回历史缓存。还有缓存条数上限控制和过期清理机制。"
|
||
|
||
---
|
||
|
||
## 七、面试问答准备
|
||
|
||
### Q1:"完成50+数据接口的前后端集成"具体做了什么?
|
||
|
||
> 我参与了两方面的工作。第一是接口定义——和 backend 协作设计并集成了 50+ 个态势感知相关的数据接口,按三个页面组织(全球态势、开源生态、武汉生态),定义了接口路径、请求方法、参数格式和返回结构。第二是搭建了完整的请求基础设施——包括 Axios 实例封装、AES-128-CBC 加密传输、Token 自动刷新 + 请求队列、HTTP 状态码统一处理、API 缓存降级系统等。全平台共集成了 641 个 API 函数,分布在 29 个文件中。
|
||
|
||
### Q2:Token 过期自动刷新是怎么实现的?
|
||
|
||
> 用了一个 `refreshing` 锁 + Promise 请求队列的机制。当请求返回 401 时,先检查 `refreshing` 是否为 true——如果是,说明已经有请求在刷新 Token 了,当前请求就包装成一个 Promise 放进等待队列;如果不是,就执行刷新并设 `refreshing = true`。刷新成功后更新 Token,然后遍历队列用新 Token 重发所有等待的请求。这样无论同时有多少个请求返回 401,都只会发一次刷新请求。
|
||
|
||
### Q3:为什么需要 AES 加密?
|
||
|
||
> 这个平台涉及安全态势数据,有一定的敏感性。后端返回的数据在传输层用 AES-128-CBC 加密,前端在响应拦截器中自动解密。加密密钥和 IV 是硬编码在前端代码中的(生产环境会用更安全的方式管理)。加密流程对业务代码完全透明——API 函数只是调用 `request()`,加密解密都在拦截器里自动完成。
|
||
|
||
### Q4:reqCatch 和 reqCatchV2 有什么区别?
|
||
|
||
> 都是 try/catch 的通用封装,避免每个页面都写 try/catch。区别在于:`reqCatch` 静默捕获错误,返回 `{ data, error }` 结构,让调用方自己判断。`reqCatchV2` 在捕获错误后还会通过事件总线 `emitEvent('responseError', e)` 通知全局——这主要用于路由守卫里,发生 403/404 时通过事件触发全局的页面跳转。新代码基本都用 V2。
|
||
|
||
### Q5:API 缓存降级是怎么运作的?
|
||
|
||
> 这是一套自研的请求降级系统。基于 IndexedDB 做持久化缓存。请求成功时自动缓存响应数据;请求失败时判断是否在熔断时间内——如果是,直接读缓存返回,用户看到的是旧数据而不是错误页面。还有最大缓存条数限制和过期清理机制。通过 `storage-apis.ts` 配置哪些接口走缓存策略,可以设置超时、重试次数、熔断时间等参数。
|
||
|
||
### Q6:HTTP 错误怎么统一处理的?
|
||
|
||
> 在响应拦截器里统一处理。根据 HTTP 状态码映射不同的行为:400 返回参数错误提示、401 触发 Token 刷新或登出、403 触发页面刷新、404 静默不提示、5xx 显示服务端错误提示。错误提示用 `dealWarning` 函数包裹了 200ms 防抖,避免短时间内多个请求同时失败时弹出多条错误消息。还有 `customError: true` 参数,允许特定请求跳过默认错误处理,由调用方自行处理。
|
||
|
||
---
|
||
|
||
## 八、关键数字(面试时用)
|
||
|
||
| 数据 | 数值 |
|
||
|------|------|
|
||
| 全平台 API 函数总数 | **641 个** |
|
||
| API 文件数 | **29 个** |
|
||
| 态势感知(金银湖)API 数 | **106 个** |
|
||
| 态势感知看板接口(situation.ts) | **50 个** |
|
||
| 请求基础设施代码量 | `request.ts` 273 行 + `degradeInterceptor.ts` 360 行 + `catch.ts` 46 行 + `status.ts` 76 行 |
|
||
| Token 刷新机制 | refreshing 锁 + Promise 队列,确保多并发 401 只刷新一次 |
|
||
| 加密方式 | AES-128-CBC,密钥 mXzfCSPBKmEA8aLq,IV tbeJJLC6dZQXXtWr |
|
||
| 请求超时 | 30 秒 |
|
||
| 错误提示防抖 | 200ms |
|
||
|
||
---
|
||
|
||
## 九、涉及的源码文件(需要看的时候查)
|
||
|
||
| 做什么 | 文件在哪 |
|
||
|--------|----------|
|
||
| 请求核心基础设施 | `src/utils/request.ts`(273 行,Axios、拦截器、加密、Token 刷新) |
|
||
| API 缓存降级系统 | `src/utils/degradeInterceptor.ts`(360 行,IndexedDB 缓存、熔断、重试) |
|
||
| 错误捕获包装函数 | `src/utils/catch.ts`(reqCatch + reqCatchV2) |
|
||
| HTTP 状态码处理 | `src/utils/status.ts`(dealWarning + showMessage) |
|
||
| 缓存配置 | `src/api/storage-apis.ts`(哪些接口走缓存策略) |
|
||
| 缓存存储层 | `src/utils/apiStorage.ts`(IndexedDB 封装) |
|
||
| 态势感知看板 API(50 端点) | `src/api/jyh/situation.ts` |
|
||
| 金银湖核心 API(45 端点) | `src/api/jyh/index.ts` |
|
||
| 金银湖首页 API(5 端点) | `src/api/jyh/home.ts` |
|
||
| 金银湖认证 API(3 端点) | `src/api/jyh/auth.ts` |
|
||
| 仓库管理 API(161 端点) | `src/api/repo/index.ts` |
|
||
| 组织管理 API(55 端点) | `src/api/org/index.ts` |
|
||
| 用户管理 API(55+ 端点) | `src/api/user/index.ts` |
|
||
| 合并请求 API(62 端点) | `src/api/merge/index.ts` |
|
||
| 讨论系统 API(50 端点) | `src/api/discussion/index.ts` |
|