feat: 支持飞书表格insert操作 & 优化netTool
All checks were successful
Egg CI/CD / build-image (push) Successful in 28s
Egg CI/CD / deploy (push) Successful in 21s

This commit is contained in:
zhaoyingbo 2024-07-12 09:37:42 +00:00
parent 9821ae9610
commit 98dd30a572
9 changed files with 373 additions and 86 deletions

View File

@ -1,6 +1,7 @@
import { manageBotReq } from "./routes/bot";
import { manageMessageReq } from "./routes/message";
import { manageMicroAppReq } from "./routes/microApp";
import { manageSheetReq } from "./routes/sheet";
import { initSchedule } from "./schedule";
initSchedule();
@ -15,6 +16,8 @@ const server = Bun.serve({
if (url.pathname === "/bot") return await manageBotReq(req);
// 消息代理发送
if (url.pathname === "/message") return await manageMessageReq(req);
// 表格代理操作
if (url.pathname === "/sheet") return await manageSheetReq(req);
// 小程序
if (url.pathname.startsWith("/micro_app"))
return await manageMicroAppReq(req);

57
routes/sheet/index.ts Normal file
View File

@ -0,0 +1,57 @@
import db from "../../db";
import service from "../../services";
import netTool from "../../services/netTool";
import { Sheet } from "../../types/sheet";
const validateSheetReq = async (body: Sheet.Body) => {
if (!body.api_key) {
return netTool.badRequest("api_key is required");
}
if (!body.sheet_token) {
return netTool.badRequest("sheet_token is required");
}
if (!body.range) {
return netTool.badRequest("range is required");
}
if (!body.values) {
return netTool.badRequest("values is required");
}
if (!Sheet.isType(body.type)) {
return netTool.badRequest("type is invalid");
}
return false;
};
export const manageSheetReq = async (req: Request) => {
const body = (await req.json()) as Sheet.Body;
// 校验参数
const validateRes = await validateSheetReq(body);
if (validateRes) return validateRes;
// 校验api_key
const apiKeyInfo = await db.apiKey.getOne(body.api_key);
if (!apiKeyInfo) {
return netTool.notFound("api key not found");
}
// 获取 app name
const appName = apiKeyInfo.expand?.app?.name;
if (!appName) {
return netTool.notFound("app name not found");
}
if (body.type === "insert") {
// 插入行
const insertRes = await service.lark.sheet.insterRows(appName)(
body.sheet_token,
body.range,
body.values
);
// 返回
return netTool.ok(insertRes);
}
return netTool.ok();
};

View File

@ -1,11 +1,13 @@
import message from "./message";
import user from "./user";
import drive from "./drive";
import sheet from "./sheet";
const lark = {
message,
user,
drive,
sheet,
};
export default lark;

View File

@ -2,31 +2,42 @@ import db from "../../db";
import { DB, LarkServer } from "../../types";
import netTool from "../netTool";
/**
* Promise
* @param url - URL
* @param method - 使HTTP方法
* @param queryParams - URL中的查询参数
* @param payload -
* @param additionalHeaders -
* @param appName -
* @returns Promise
* @throws
*/
const larkNetTool = async <T = LarkServer.BaseRes>({
url,
method,
params,
data,
headers,
queryParams,
payload,
additionalHeaders,
appName = "egg",
}: {
url: string;
method: string;
params?: any;
data?: any;
headers?: any;
queryParams?: any;
payload?: any;
additionalHeaders?: any;
appName?: string;
}): Promise<T> => {
const headersWithAuth = {
Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`,
...headers,
...additionalHeaders,
};
return netTool<T>({
url,
method,
params,
data,
headers: headersWithAuth,
queryParams,
payload,
additionalHeaders: headersWithAuth,
}).catch((error) => {
console.error("larkNetTool catch error: ", error);
return {
@ -37,33 +48,78 @@ const larkNetTool = async <T = LarkServer.BaseRes>({
});
};
/**
* GET请求并返回一个解析为响应数据的Promise
*
* @param appName -
* @returns URLPromise
*/
larkNetTool.get =
(appName: string = "egg") =>
(appName?: string) =>
<T = LarkServer.BaseRes>(
url: string,
params?: any,
headers?: any
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
larkNetTool({ url, method: "get", params, headers, appName });
larkNetTool({
url,
method: "get",
queryParams,
additionalHeaders,
appName,
});
/**
* POST请求并返回一个解析为响应数据的Promise
*
* @param appName -
* @returns URLPromise
*/
larkNetTool.post =
(appName: string = "egg") =>
(appName?: string) =>
<T = LarkServer.BaseRes>(
url: string,
data?: any,
params?: any,
headers?: any
payload?: any,
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
larkNetTool({ url, method: "post", data, params, headers, appName });
larkNetTool({
url,
method: "post",
payload,
queryParams,
additionalHeaders,
appName,
});
/**
* DELETE请求并返回一个解析为响应数据的Promise
*
* @param appName -
* @returns URLPromise
*/
larkNetTool.del =
(appName: string = "egg") =>
<T = LarkServer.BaseRes>(url: string, data: any, headers?: any): Promise<T> =>
larkNetTool({ url, method: "delete", data, headers, appName });
(appName?: string) =>
<T = LarkServer.BaseRes>(
url: string,
payload: any,
additionalHeaders?: any
): Promise<T> =>
larkNetTool({ url, method: "delete", payload, additionalHeaders, appName });
/**
* PATCH请求并返回一个解析为响应数据的Promise
*
* @param appName -
* @returns URLPromise
*/
larkNetTool.patch =
(appName: string = "egg") =>
<T = LarkServer.BaseRes>(url: string, data: any, headers?: any): Promise<T> =>
larkNetTool({ url, method: "patch", data, headers, appName });
(appName?: string) =>
<T = LarkServer.BaseRes>(
url: string,
payload: any,
additionalHeaders?: any
): Promise<T> =>
larkNetTool({ url, method: "patch", payload, additionalHeaders, appName });
export default larkNetTool;

View File

@ -1,4 +1,3 @@
import { DB } from "../../types";
import { LarkServer } from "../../types/larkServer";
import larkNetTool from "./larkNetTool";

View File

@ -0,0 +1,25 @@
import { LarkServer } from "../../types/larkServer";
import larkNetTool from "./larkNetTool";
/**
*
* @param appName -
* @returns
*/
const insterRows =
(appName?: string) =>
async (sheetToken: string, range: string, values: string[][]) => {
const URL = `https://open.f.mioffice.cn/open-apis/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS`;
return larkNetTool.post(appName)<LarkServer.BaseRes>(URL, {
valueRange: {
range,
values,
},
});
};
const sheet = {
insterRows,
};
export default sheet;

View File

@ -1,29 +1,27 @@
interface NetGetParams {
interface NetRequestParams {
url: string;
method: string;
params?: any;
data?: any;
headers?: any;
queryParams?: any;
payload?: any;
additionalHeaders?: any;
}
/**
*
* @param response
* @param method
* @param data
*
* @param response -
* @param method - 使HTTP方法
* @param headers -
* @param requestBody -
* @param responseBody -
* @returns
*/
const logResponse = async (
const logResponse = (
response: Response,
method: string,
data: any,
headers: any
headers: any,
requestBody: any,
responseBody: any
) => {
let responseData = null;
try {
responseData = await response.json();
} catch (error) {
responseData = "parse to json error";
}
const responseLog = {
ok: response.ok,
status: response.status,
@ -32,76 +30,190 @@ const logResponse = async (
method: method,
requestHeaders: headers,
responseHeaders: response.headers,
requestBody: data,
responseBody: responseData as any,
requestBody,
responseBody,
};
console.log("🚀 ~ responseLog:", JSON.stringify(responseLog, null, 2));
return responseLog;
};
const netTool = <T = any>({
/**
* Promise
* @param url - URL
* @param method - 使HTTP方法
* @param queryParams - URL中的查询参数
* @param payload -
* @param additionalHeaders -
* @returns Promise
* @throws
*/
const netTool = async <T = any>({
url,
method,
params,
data,
headers,
}: NetGetParams): Promise<T> => {
queryParams,
payload,
additionalHeaders,
}: NetRequestParams): Promise<T> => {
// 拼接完整的URL
let fullUrl = url;
if (params) {
if (typeof params === "string") {
fullUrl = `${url}?${params}`;
if (queryParams) {
if (typeof queryParams === "string") {
fullUrl = `${url}?${queryParams}`;
} else {
const queryString = new URLSearchParams(params).toString();
fullUrl = `${url}?${queryString}`;
const queryString = new URLSearchParams(queryParams).toString();
if (queryString) fullUrl = `${url}?${queryString}`;
}
}
return fetch(fullUrl, {
// 设置请求头
const headers = {
"Content-Type": "application/json",
...additionalHeaders,
};
// 发送请求
const res = await fetch(fullUrl, {
method,
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
...headers,
},
})
.then((response) => logResponse(response, method, data, headers))
.then((responseLog) => {
if (!responseLog.ok) {
if (responseLog?.responseBody?.msg) {
throw new Error(responseLog.responseBody.msg);
}
throw new Error("网络响应异常");
}
if (responseLog.responseBody === "parse to json error") {
throw new Error("解析响应数据异常");
}
return responseLog.responseBody as T;
});
body: JSON.stringify(payload),
headers,
});
// 获取响应数据
let resData: any = null;
try {
resData = await res.json();
} catch (error) {
resData = "解析为JSON时出错";
}
// 记录响应
logResponse(res, method, headers, payload, resData);
if (!res.ok) {
if (resData?.msg) {
throw new Error(resData.msg);
}
throw new Error("网络响应异常");
}
if (resData === "解析为JSON时出错") {
throw new Error("解析响应数据异常");
}
return resData as T;
};
netTool.get = <T = any>(url: string, params?: any, headers?: any): Promise<T> =>
netTool({ url, method: "get", params, headers });
/**
* GET请求并返回一个解析为响应数据的Promise
*
* @param url - URL
* @param queryParams - URL中的查询参数
* @param additionalHeaders -
* @returns Promise
*/
netTool.get = <T = any>(
url: string,
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
netTool({ url, method: "get", queryParams, additionalHeaders });
/**
* POST请求并返回一个解析为响应数据的Promise
*
* @param url - URL
* @param payload -
* @param queryParams - URL中的查询参数
* @param additionalHeaders -
* @returns Promise
*/
netTool.post = <T = any>(
url: string,
data?: any,
params?: any,
headers?: any
): Promise<T> => netTool({ url, method: "post", data, params, headers });
payload?: any,
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
netTool({ url, method: "post", payload, queryParams, additionalHeaders });
netTool.del = <T = any>(url: string, data: any, headers?: any): Promise<T> =>
netTool({ url, method: "delete", data, headers });
/**
* PUT请求并返回一个解析为响应数据的Promise
*
* @param url - URL
* @param payload -
* @param queryParams - URL中的查询参数
* @param additionalHeaders -
* @returns Promise
*/
netTool.put = <T = any>(
url: string,
payload: any,
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
netTool({ url, method: "put", payload, queryParams, additionalHeaders });
netTool.patch = <T = any>(url: string, data: any, headers?: any): Promise<T> =>
netTool({ url, method: "patch", data, headers });
/**
* DELETE请求并返回一个解析为响应数据的Promise
*
* @param url - URL
* @param payload -
* @param queryParams - URL中的查询参数
* @param additionalHeaders -
* @returns Promise
*/
netTool.del = <T = any>(
url: string,
payload: any,
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
netTool({ url, method: "delete", payload, queryParams, additionalHeaders });
/**
* PATCH请求并返回一个解析为响应数据的Promise
*
* @param url - URL
* @param payload -
* @param queryParams - URL中的查询参数
* @param additionalHeaders -
* @returns Promise
*/
netTool.patch = <T = any>(
url: string,
payload: any,
queryParams?: any,
additionalHeaders?: any
): Promise<T> =>
netTool({ url, method: "patch", payload, queryParams, additionalHeaders });
/**
* 400 Bad Request的响应对象
*
* @param msg -
* @returns 400 Bad Request的响应对象
*/
netTool.badRequest = (msg: string) => new Response(msg, { status: 400 });
/**
* 404 Not Found的响应对象
*
* @param msg -
* @returns 404 Not Found的响应对象
*/
netTool.notFound = (msg: string) => new Response(msg, { status: 404 });
/**
* 500 Internal Server Error的响应对象
*
* @param msg -
* @param data -
* @returns 500 Internal Server Error的响应对象
*/
netTool.serverError = (msg: string, data: any) =>
Response.json({ code: 500, msg, data }, { status: 500 });
netTool.ok = (data: any) => Response.json({ code: 200, msg: "ok", data });
/**
* 200 OK的响应对象
*
* @param data -
* @returns 200 OK的响应对象
*/
netTool.ok = (data?: any) => Response.json({ code: 200, msg: "ok", data });
export default netTool;

15
test/insertSheet.ts Normal file
View File

@ -0,0 +1,15 @@
// const URL = "https://egg.imoaix.cn/sheet";
const URL = "http://localhost:3000/sheet";
const res = await fetch(URL, {
method: "POST",
body: JSON.stringify({
api_key: "uwnpzb9hvoft28h",
sheet_token: "shtk48HuiHQOUSTAZ0t0DFplNJc",
type: "insert",
range: "a2YJxa",
values: [["hello", "world"]],
}),
});
console.log(JSON.stringify(await res.text()));

18
types/sheet.ts Normal file
View File

@ -0,0 +1,18 @@
export namespace Sheet {
export enum Type {
Insert = "insert",
}
export interface Body {
api_key: string;
sheet_token: string;
type: "insert";
range: string;
values: string[][]; // 二维数组
}
// 判断一个值是否是枚举中的值
export const isType = (value: any): value is Type => {
return Object.values(Type).includes(value);
};
}