Skip to content

Commit

Permalink
feat: add request library and cache hook, automatically generate requ…
Browse files Browse the repository at this point in the history
…ests through interface documentation
  • Loading branch information
1379255913 committed Dec 16, 2024
1 parent a7ddf17 commit a66431e
Show file tree
Hide file tree
Showing 19 changed files with 1,926 additions and 42 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,16 @@ In the output, you'll find options to open the app in a
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo

You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).

## 更新接口文档后自动生成请求

已有 Apifox 接口文档,生成请求client,推荐后端给每个接口都打上 tags,不要使用 apifox 的 API文档目录 作为 tags

1. 打开 Apifox 桌面客户端
2. 选择需要查阅 API 文档的服务,点击进入
3. 点击服务左侧工具栏目中的 项目设置
4. 点击 导出数据
5. 选择 OpenAPI Spec 版本:OpenAPI3.0 ,文件格式:JSON,包含 Apifox 扩展的 OpenAPI 字段(x-apifox-***):包含,将 API 文档的目录,作为 Tags 字段导出:否
6. 点击 打开URL 按钮,会生成类似以下的临时的接口文档链接:http://127.0.0.1:4523/export/openapi/2?version=3.0
7. 修改 openapi-ts-request.config.ts 文件中的schemaPath为上面的链接
8. 运行 yarn run openapi 自动生成请求代码
153 changes: 153 additions & 0 deletions api/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ResultEnum } from '@/api/enum';
import { userLogin } from '@/utils/user';
import AsyncStorage from '@react-native-async-storage/async-storage';
import axios, { AxiosRequestConfig } from 'axios';

const baseURL = 'https://fzuhelper.west2.online/';

const request = axios.create({
baseURL,
timeout: 5000,
});

interface PendingTask {
config: AxiosRequestConfig;
resolve: Function;
reject: Function;
}

let refreshing = false;
let queue: PendingTask[] = [];
// 白名单,防止重新获取时被拦截
const WhiteList = ['/api/v1/login/access-token', '/api/v1/internal/user/login'];

request.interceptors.response.use(
async response => {
let { data, config } = response;

if (refreshing && !WhiteList.includes(config.url || '')) {
return new Promise((resolve, reject) => {
queue.push({
config,
resolve,
reject,
});
});
}

// accessToken过期
if (data.code === ResultEnum.AuthAccessExpiredCode) {
refreshing = true;

// 尝试刷新token
try {
const res = await axios.get('/api/v1/login/refresh-token', {
baseURL,
timeout: 5000,
headers: {
Authorization: await AsyncStorage.getItem('refresh_token'),
},
});
const { 'access-token': accessToken, 'refresh-token': refreshToken } =
res.headers;
console.log(accessToken, refreshToken);
accessToken &&
(await AsyncStorage.setItem('access_token', accessToken));
refreshToken &&
(await AsyncStorage.setItem('refresh_token', refreshToken));

queue.forEach(({ config, resolve }) => {
resolve(request(config));
});
refreshing = false;
queue = [];
return request(config);
} catch (error: any) {
if (error?.data?.code === ResultEnum.AuthRefreshExpiredCode) {
//TODO 重新输入账号密码登录
} else {
queue.forEach(({ config, reject }) => {
reject(request(config));
});
refreshing = false;
queue = [];
}
}
}
// 处理jwch cookie异常
if (data.code === ResultEnum.BizJwchCookieExceptionCode) {
// TODO 尝试重新登录并获取cookies和id
const id = await AsyncStorage.getItem('user_id');
const password = await AsyncStorage.getItem('user_password');
if (id && password) {
refreshing = true;
try {
await userLogin({
id,
password,
});
queue.forEach(({ config, resolve }) => {
resolve(request(config));
});
refreshing = false;

queue = [];
return request(config);
} catch (error) {
// TODO 判断是否为密码错误
queue.forEach(({ reject }) => {
reject();
});
refreshing = false;
queue = [];
return Promise.reject();
}
}
}

// 其他错误
if (data.code !== ResultEnum.SuccessCode) {
//TODO 错误消息提示处理
return Promise.reject(response);
}

// 更新accessToken和refreshToken
const { 'access-token': accessToken, 'refresh-token': refreshToken } =
response.headers;
console.log(response.headers);
accessToken && (await AsyncStorage.setItem('access_token', accessToken));
refreshToken && (await AsyncStorage.setItem('refresh_token', refreshToken));

return response;
},
error => {
// 判断是否为超时error
if (error.message.indexOf('timeout') !== -1) {
//TODO 超时消息处理
}
// 判断是否为网络error或者不存在页面
if (error.message.indexOf('Network Error') !== -1) {
//TODO 网络错误消息处理
}

return Promise.reject(error);
},
);

request.interceptors.request.use(async function (config) {
const accessToken = await AsyncStorage.getItem('access_token');
const id = await AsyncStorage.getItem('id');
const cookies = await AsyncStorage.getItem('cookies');
if (accessToken) {
config.headers.Authorization = accessToken;
}
if (id) {
config.headers.Id = id;
}
if (cookies) {
config.headers.Cookies = cookies;
}
return config;
});

export default request;
54 changes: 54 additions & 0 deletions api/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Description: 枚举定义 https://github.com/west2-online/fzuhelper-server/blob/main/pkg/errno/code.go
export enum ResultEnum {
SuccessCode = '10000',
ParamErrorCode = '20001', // 参数错误
ParamEmptyCode = '20002', // 参数为空
ParamMissingHeaderCode = '20003', // 缺少请求头数据(id or cookies)
ParamInvalidCode = '20004', // 参数无效
ParamMissingCode = '20005', // 参数缺失
ParamTooLongCode = '20006', // 参数过长
ParamTooShortCode = '20007', // 参数过短
ParamTypeCode = '20008', // 参数类型错误
ParamFormatCode = '20009', // 参数格式错误
ParamRangeCode = '20010', // 参数范围错误
ParamValueCode = '20011', // 参数值错误
ParamFileNotExistCode = '20012', // 文件不存在
ParamFileReadErrorCode = '20013', // 文件读取错误

AuthErrorCode = '30001', // 鉴权错误
AuthInvalidCode = '30002', // 鉴权无效
AuthAccessExpiredCode = '30003', // 访问令牌过期
AuthRefreshExpiredCode = '30004', // 刷新令牌过期
AuthMissingCode = '30005', // 鉴权缺失

BizErrorCode = '40001', // 业务错误
BizLogicCode = '40002', // 业务逻辑错误
BizLimitCode = '40003', // 业务限制错误
BizNotExist = '40005', // 业务不存在错误
BizFileUploadErrorCode = '40006', // 文件上传错误(service 层)
BizJwchCookieExceptionCode = '40007', // jwch cookie异常

InternalServiceErrorCode = '50001', // 未知服务错误
InternalDatabaseErrorCode = '50002', // 数据库错误
InternalRedisErrorCode = '50003', // Redis错误
InternalNetworkErrorCode = '50004', // 网络错误
InternalTimeoutErrorCode = '50005', // 超时错误
InternalIOErrorCode = '50006', // IO错误
InternalJSONErrorCode = '50007', // JSON错误
InternalXMLErrorCode = '50008', // XML错误
InternalURLEncodeErrorCode = '50009', // URL编码错误
InternalHTTPErrorCode = '50010', // HTTP错误
InternalHTTP2ErrorCode = '50011', // HTTP2错误
InternalGRPCErrorCode = '50012', // GRPC错误
InternalThriftErrorCode = '50013', // Thrift错误
InternalProtobufErrorCode = '50014', // Protobuf错误
InternalSQLErrorCode = '50015', // SQL错误
InternalNoSQLErrorCode = '50016', // NoSQL错误
InternalORMErrorCode = '50017', // ORM错误
InternalQueueErrorCode = '50018', // 队列错误
InternalETCDErrorCode = '50019', // ETCD错误
InternalTraceErrorCode = '50020', // Trace错误

// SuccessCodePaper paper在旧版Android中的SuccessCode是2000,用作兼容
SuccessCodePaper = '2000',
}
81 changes: 81 additions & 0 deletions api/generate/academic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable */
// @ts-ignore
import * as API from './types';
import request from '../axios';

/** 获取学分统计 注意这里可能涉及到辅修 GET /api/v1/jwch/academic/credit */
export async function getApiV1JwchAcademicCredit(options?: {
[key: string]: unknown;
}) {
return request<{
code: string;
message: string;
data: { type: string; gain: string; total: string }[];
}>('/api/v1/jwch/academic/credit', {
method: 'GET',
...(options || {}),
});
}

/** 绩点排名 GET /api/v1/jwch/academic/gpa */
export async function getApiV1JwchAcademicGpa(options?: {
[key: string]: unknown;
}) {
return request<{
code: string;
message: string;
data: { time: string; data: { type: string; value: string }[] };
}>('/api/v1/jwch/academic/gpa', {
method: 'GET',
...(options || {}),
});
}

/** 获取专业培养计划 GET /api/v1/jwch/academic/plan */
export async function getApiV1JwchAcademicPlan(options?: {
[key: string]: unknown;
}) {
return request<{ code: string; message: string; data: string }>(
'/api/v1/jwch/academic/plan',
{
method: 'GET',
...(options || {}),
}
);
}

/** 成绩详情 GET /api/v1/jwch/academic/scores */
export async function getApiV1JwchAcademicScores(options?: {
[key: string]: unknown;
}) {
return request<{
code: string;
message: string;
data: {
credit: string;
gpa: string;
name: string;
score: string;
teacher: string;
term: string;
year: string;
}[];
}>('/api/v1/jwch/academic/scores', {
method: 'GET',
...(options || {}),
});
}

/** 统考成绩 CET、省计算机 GET /api/v1/jwch/academic/unified-exam */
export async function getApiV1JwchAcademicUnifiedExam(options?: {
[key: string]: unknown;
}) {
return request<{
code: string;
message: string;
data: { name: string; score: string; term: string }[];
}>('/api/v1/jwch/academic/unified-exam', {
method: 'GET',
...(options || {}),
});
}
38 changes: 38 additions & 0 deletions api/generate/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable */
// @ts-ignore
import * as API from './types';
import request from '../axios';

/** 测试拦截功能 测试/api路由下接口能否被正确拦截 GET /api/v1/jwch/ping */
export async function getApiV1JwchPing(options?: { [key: string]: unknown }) {
return request<{ code: string; message: string }>('/api/v1/jwch/ping', {
method: 'GET',
...(options || {}),
});
}

/** 获取 token 通过在header中提供id和cookies来获取token GET /api/v1/login/access-token */
export async function getApiV1LoginAccessToken(options?: {
[key: string]: unknown;
}) {
return request<{ code: string; message: string }>(
'/api/v1/login/access-token',
{
method: 'GET',
...(options || {}),
}
);
}

/** 刷新 token 通过在 header 中提供 refreshtoken(长期) 来刷新 accesstoken(短期) GET /api/v1/login/refresh-token */
export async function getApiV1LoginRefreshToken(options?: {
[key: string]: unknown;
}) {
return request<{ code: string; message: string }>(
'/api/v1/login/refresh-token',
{
method: 'GET',
...(options || {}),
}
);
}
Loading

0 comments on commit a66431e

Please sign in to comment.