feat: 支持多租户使用
All checks were successful
Egg CI/CD / build-image (push) Successful in 1m6s
Egg CI/CD / deploy (push) Successful in 1m7s

This commit is contained in:
zhaoyingbo 2024-06-14 04:34:37 +00:00
parent 208ea1c538
commit d9d758c6ee
16 changed files with 197 additions and 213 deletions

View File

@ -24,7 +24,8 @@
"ChakrounAnas.turbo-console-log",
"Gruntfuggly.todo-tree",
"MS-CEINTL.vscode-language-pack-zh-hans",
"GitHub.copilot"
"GitHub.copilot",
"GitHub.copilot-chat"
]
}
},

3
constant.ts Normal file
View File

@ -0,0 +1,3 @@
import { DB } from "./types";
export const supportApp: DB.AppName[] = ["egg", "seek", "phone"];

View File

@ -12,12 +12,18 @@ const get = async (key: string) => {
async () =>
await await pbClient.collection("config").getFirstListItem(`key='${key}'`)
);
return config;
};
const getVal = async (key: string) => {
const config = await get(key);
if (!config) return "";
return config.value;
};
const appConfig = {
get,
getVal,
};
export default appConfig;

View File

@ -1,52 +1,42 @@
import { DB } from "../../types";
import appConfig from "../appConfig";
import pbClient from "../pbClient";
let eggToken = "";
let seekToken = "";
const tokenCache = {} as Record<DB.AppName, string>;
/**
* token
* @param {DB.AppName} appName
* @param {string} value token
*/
const update = async (value: string) => {
await pbClient.collection("config").update("ugel8f0cpk0rut6", { value });
eggToken = value;
const update = async (appName: DB.AppName, value: string) => {
const config = await appConfig.get(`${appName}_tenant_access_token`);
if (!config) {
await pbClient.collection("config").create({
key: `${appName}_tenant_access_token`,
value,
});
} else {
await pbClient.collection("config").update(config.id, { value });
}
tokenCache[appName] = value;
console.log("reset egg access token success", value);
};
/**
* seek的token
* @param {string} value token
*/
const updateSeek = async (value: string) => {
await pbClient.collection("config").update("2qor9q9esn5ouqg", { value });
seekToken = value;
console.log("reset seek access token success", value);
};
/**
* token
* @returns {string} token
*/
const get = async () => {
if (eggToken) return eggToken;
return await appConfig.get("tenant_access_token");
};
/**
* seek的token
* @returns {string} seek的token
*/
const getSeek = async () => {
if (seekToken) return seekToken;
return await appConfig.get("seek_tenant_access_token");
const get = async (appName: DB.AppName) => {
if (tokenCache[appName]) return tokenCache[appName];
const config = await appConfig.getVal(`${appName}_tenant_access_token`);
tokenCache[appName] = config;
return config;
};
const tenantAccessToken = {
get,
update,
getSeek,
updateSeek,
};
export default tenantAccessToken;

View File

@ -42,7 +42,7 @@ const manageBtnClick = async (body: LarkAction.Data) => {
const card = await func(body);
if (!card) return;
// 更新飞书的卡片
await service.lark.message.update(body.open_message_id, card);
await service.lark.message.update()(body.open_message_id, card);
};
/**

View File

@ -48,7 +48,7 @@ const filterIllegalMsg = (body: LarkEvent.Data) => {
// 发表情包就直接发回去
if (msgType === "sticker") {
const content = body?.event?.message?.content;
service.lark.message.send("chat_id", chatId, "sticker", content);
service.lark.message.send()("chat_id", chatId, "sticker", content);
}
// 非表情包只在私聊或者群聊中艾特小煎蛋时才回复
@ -56,7 +56,7 @@ const filterIllegalMsg = (body: LarkEvent.Data) => {
const content = JSON.stringify({
text: "哇!这是什么东东?我只懂普通文本啦![可爱]",
});
service.lark.message.send("chat_id", chatId, "text", content);
service.lark.message.send()("chat_id", chatId, "text", content);
}
// 非纯文本,全不放行
@ -80,7 +80,7 @@ const manageIdMsg = async (chatId: string) => {
},
},
});
service.lark.message.send("chat_id", chatId, "interactive", content);
service.lark.message.send()("chat_id", chatId, "interactive", content);
};
/**
@ -105,7 +105,7 @@ const manageCMDMsg = (body: LarkEvent.Data) => {
const content = JSON.stringify({
text: "正在为您收集简报,请稍等片刻~",
});
service.lark.message.send("chat_id", chatId, "text", content);
service.lark.message.send()("chat_id", chatId, "text", content);
return true;
}
return false;
@ -130,7 +130,7 @@ const replyGuideMsg = async (body: LarkEvent.Data) => {
},
},
});
await service.lark.message.send("chat_id", chatId, "interactive", content);
await service.lark.message.send()("chat_id", chatId, "interactive", content);
};
/**

View File

@ -1,8 +1,8 @@
import { supportApp } from "../../constant";
import db from "../../db";
import service from "../../services";
import { LarkServer, MsgProxy } from "../../types";
const validateMessageReq = (body: MsgProxy.Body) => {
if (!body.group_id && !body.receive_id) {
return new Response("group_id or receive_id is required", { status: 400 });
@ -16,6 +16,9 @@ const validateMessageReq = (body: MsgProxy.Body) => {
if (!body.content) {
return new Response("content is required", { status: 400 });
}
if (body.app_name && !supportApp.includes(body.app_name)) {
return new Response("app_name is invalid", { status: 400 });
}
return false;
};
@ -57,7 +60,12 @@ export const manageMessageReq = async (req: Request) => {
return (receive_id: string) => {
sendList.push(
service.lark.message
.send(receive_id_type, receive_id, body.msg_type, finalContent)
.send(body.app_name)(
receive_id_type,
receive_id,
body.msg_type,
finalContent
)
.then((res) => {
sendRes[receive_id_type][receive_id] = res;
})
@ -76,7 +84,7 @@ export const manageMessageReq = async (req: Request) => {
if (body.receive_id && body.receive_id_type) {
sendList.push(
service.lark.message
.send(
.send(body.app_name)(
body.receive_id_type,
body.receive_id,
body.msg_type,

View File

@ -1,14 +1,21 @@
import { supportApp } from "../../constant";
import service from "../../services";
import { DB } from "../../types";
import { trimPathPrefix } from "../../utils/pathTools";
/**
*
* @param req
* @returns
*/
const manageLogin = async (req: Request, isSeek = false) => {
const manageLogin = async (req: Request) => {
const url = new URL(req.url);
const code = url.searchParams.get("code");
console.log("🚀 ~ manageLogin ~ code:", code);
const appName =
(url.searchParams.get("app_name") as DB.AppName | null) || undefined;
if (appName && !supportApp.includes(appName)) {
return new Response("app_name is invalid", { status: 400 });
}
if (!code) {
return new Response("code not found", { status: 400 });
}
@ -16,8 +23,10 @@ const manageLogin = async (req: Request, isSeek = false) => {
code: resCode,
data,
msg,
} = await service.lark.user.code2Login(code, isSeek);
} = await service.lark.user.code2Login(appName)(code);
console.log("🚀 ~ manageLogin:", resCode, data, msg);
if (resCode !== 0) {
return Response.json({
code: resCode,
@ -38,21 +47,25 @@ const manageLogin = async (req: Request, isSeek = false) => {
* @param req
* @returns
*/
const manageBatchUser = async (req: Request, isSeek = false) => {
const manageBatchUser = async (req: Request) => {
const body = (await req.json()) as any;
console.log("🚀 ~ manageBatchUser ~ body:", body);
const { user_ids, user_id_type } = body;
const { user_ids, user_id_type, app_name } = body;
if (!user_ids) {
return new Response("user_ids not found", { status: 400 });
}
if (!user_id_type) {
return new Response("user_id_type not found", { status: 400 });
}
const { code, data, msg } = await service.lark.user.batchGet(
if (app_name && !supportApp.includes(app_name)) {
return new Response("app_name is invalid", { status: 400 });
}
const { code, data, msg } = await service.lark.user.batchGet(app_name)(
user_ids,
user_id_type,
isSeek
user_id_type
);
console.log("🚀 ~ manageBatchUser:", code, data, msg);
if (code !== 0) {
return Response.json({
@ -75,22 +88,14 @@ const manageBatchUser = async (req: Request, isSeek = false) => {
*/
export const manageMicroAppReq = async (req: Request) => {
const url = new URL(req.url);
const withoutPrefix = trimPathPrefix(url.pathname, "/micro_app");
// 处理登录请求
if (url.pathname === "/micro_app/egg/login") {
if (withoutPrefix === "/login") {
return manageLogin(req);
}
// 处理批量获取用户信息请求
if (url.pathname === "/micro_app/egg/batch_user") {
if (withoutPrefix === "/batch_user") {
return manageBatchUser(req);
}
// 处理Seek的登录请求
if (url.pathname === "/micro_app/seek/login") {
return manageLogin(req, true);
}
// 处理Seek的批量获取用户信息请求
if (url.pathname === "/micro_app/seek/batch_user") {
return manageBatchUser(req, true);
}
return new Response("hello, glade to see you!");
};

View File

@ -1,26 +1,23 @@
import { supportApp } from "../constant";
import db from "../db";
import netTool from "../services/netTool";
import { DB } from "../types";
const URL =
"https://open.f.mioffice.cn/open-apis/auth/v3/tenant_access_token/internal";
const resetEggAccessToken = async () => {
const refreshAccessToken = async (appName: DB.AppName) => {
const appId = await db.appConfig.getVal(`${appName}_app_id`);
const appSecret = await db.appConfig.getVal(`${appName}_app_secret`);
const { tenant_access_token } = await netTool.post(URL, {
app_id: await db.appConfig.get("app_id"),
app_secret: await db.appConfig.get("app_secret"),
app_id: appId,
app_secret: appSecret,
});
db.tenantAccessToken.update(tenant_access_token);
};
const resetSeekAccessToken = async () => {
const { tenant_access_token } = await netTool.post(URL, {
app_id: await db.appConfig.get("seek_app_id"),
app_secret: await db.appConfig.get("seek_app_secret"),
});
db.tenantAccessToken.updateSeek(tenant_access_token);
db.tenantAccessToken.update(appName, tenant_access_token);
};
export const resetAccessToken = async () => {
resetEggAccessToken();
resetSeekAccessToken();
for (const appName of supportApp) {
await refreshAccessToken(appName);
}
};

View File

@ -1,5 +1,5 @@
import db from "../../db";
import { LarkServer } from "../../types";
import { DB, LarkServer } from "../../types";
import netTool from "../netTool";
const larkNetTool = async <T = LarkServer.BaseRes>({
@ -8,15 +8,17 @@ const larkNetTool = async <T = LarkServer.BaseRes>({
params,
data,
headers,
appName = "egg",
}: {
url: string;
method: string;
params?: any;
data?: any;
headers?: any;
appName?: DB.AppName;
}): Promise<T> => {
const headersWithAuth = {
Authorization: `Bearer ${await db.tenantAccessToken.get()}`,
Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`,
...headers,
};
return netTool<T>({
@ -35,29 +37,33 @@ const larkNetTool = async <T = LarkServer.BaseRes>({
});
};
larkNetTool.get = <T = LarkServer.BaseRes>(
url: string,
params?: any,
headers?: any
): Promise<T> => larkNetTool({ url, method: "get", params, headers });
larkNetTool.get =
(appName: DB.AppName = "egg") =>
<T = LarkServer.BaseRes>(
url: string,
params?: any,
headers?: any
): Promise<T> =>
larkNetTool({ url, method: "get", params, headers, appName });
larkNetTool.post = <T = LarkServer.BaseRes>(
url: string,
data?: any,
params?: any,
headers?: any
): Promise<T> => larkNetTool({ url, method: "post", data, params, headers });
larkNetTool.post =
(appName: DB.AppName = "egg") =>
<T = LarkServer.BaseRes>(
url: string,
data?: any,
params?: any,
headers?: any
): Promise<T> =>
larkNetTool({ url, method: "post", data, params, headers, appName });
larkNetTool.del = <T = LarkServer.BaseRes>(
url: string,
data: any,
headers?: any
): Promise<T> => larkNetTool({ url, method: "delete", data, headers });
larkNetTool.del =
(appName: DB.AppName = "egg") =>
<T = LarkServer.BaseRes>(url: string, data: any, headers?: any): Promise<T> =>
larkNetTool({ url, method: "delete", data, headers, appName });
larkNetTool.patch = <T = LarkServer.BaseRes>(
url: string,
data: any,
headers?: any
): Promise<T> => larkNetTool({ url, method: "patch", data, headers });
larkNetTool.patch =
(appName: DB.AppName = "egg") =>
<T = LarkServer.BaseRes>(url: string, data: any, headers?: any): Promise<T> =>
larkNetTool({ url, method: "patch", data, headers, appName });
export default larkNetTool;

View File

@ -1,3 +1,4 @@
import { DB } from "../../types";
import { LarkServer } from "../../types/larkServer";
import larkNetTool from "./larkNetTool";
@ -8,29 +9,32 @@ import larkNetTool from "./larkNetTool";
* @param {MsgType} msg_type textpostimagefileaudiomediastickerinteractiveshare_chatshare_user
* @param {string} content JSON结构序列化后的字符串msg_type对应不同内容
*/
const send = async (
receive_id_type: LarkServer.ReceiveIDType,
receive_id: string,
msg_type: LarkServer.MsgType,
content: string
) => {
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`;
return larkNetTool.post<LarkServer.BaseRes>(URL, {
receive_id,
msg_type,
content,
});
};
const send =
(appName?: DB.AppName) =>
async (
receive_id_type: LarkServer.ReceiveIDType,
receive_id: string,
msg_type: LarkServer.MsgType,
content: string
) => {
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`;
return larkNetTool.post(appName)<LarkServer.BaseRes>(URL, {
receive_id,
msg_type,
content,
});
};
/**
*
* @param {string} message_id id
* @param {string} content JSON结构序列化后的字符串msg_type对应不同内容
*/
const update = async (message_id: string, content: string) => {
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages/${message_id}`;
return larkNetTool.patch<LarkServer.BaseRes>(URL, { content });
};
const update =
(appName?: DB.AppName) => async (message_id: string, content: string) => {
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages/${message_id}`;
return larkNetTool.patch(appName)<LarkServer.BaseRes>(URL, { content });
};
const message = {
send,

View File

@ -1,4 +1,5 @@
import db from "../../db";
import { DB } from "../../types";
import { LarkServer } from "../../types/larkServer";
import larkNetTool from "./larkNetTool";
@ -7,19 +8,9 @@ import larkNetTool from "./larkNetTool";
* @param code
* @returns
*/
const code2Login = async (code: string, isSeek = false) => {
const code2Login = (appName?: DB.AppName) => async (code: string) => {
const URL = `https://open.f.mioffice.cn/open-apis/mina/v2/tokenLoginValidate`;
const headers = isSeek
? {
Authorization: `Bearer ${await db.tenantAccessToken.getSeek()}`,
}
: {};
return larkNetTool.post<LarkServer.UserSessionRes>(
URL,
{ code },
undefined,
headers
);
return larkNetTool.post(appName)<LarkServer.UserSessionRes>(URL, { code });
};
/**
@ -27,96 +18,57 @@ const code2Login = async (code: string, isSeek = false) => {
* @param user_id
* @returns
*/
const get = async (user_id: string, user_id_type: string, isSeek = false) => {
const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/${user_id}`;
const headers = isSeek
? {
Authorization: `Bearer ${await db.tenantAccessToken.getSeek()}`,
}
: {};
return larkNetTool.get<LarkServer.UserInfoRes>(
URL,
{
const get =
(appName?: DB.AppName) => async (user_id: string, user_id_type: string) => {
const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/${user_id}`;
return larkNetTool.get(appName)<LarkServer.UserInfoRes>(URL, {
user_id_type,
},
headers
);
};
});
};
/**
* 使get接口模拟批量获取用户信息
*
* @param user_ids
* @returns
*/
const batchGet = async (
user_ids: string[],
user_id_type: "open_id" | "user_id",
isSeek = false
) => {
const requestMap = user_ids.map((user_id) => {
return get(user_id, user_id_type, isSeek);
});
const responses = await Promise.all(requestMap);
const items = responses.map((res) => {
return res.data.user;
});
return {
code: 0,
data: {
items,
},
msg: "success",
const batchGet =
(appName?: DB.AppName) =>
async (user_ids: string[], user_id_type: "open_id" | "user_id") => {
const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/batch`;
// 如果user_id长度超出50需要分批请求
const user_idsLen = user_ids.length;
const maxLen = 50;
const requestMap = Array.from(
{ length: Math.ceil(user_idsLen / maxLen) },
(_, index) => {
const start = index * maxLen;
const user_idsSlice = user_ids.slice(start, start + maxLen);
const getParams = `${user_idsSlice
.map((id) => `user_ids=${id}`)
.join("&")}&user_id_type=${user_id_type}`;
return larkNetTool.get(appName)<LarkServer.BatchUserInfoRes>(
URL,
getParams
);
}
);
const responses = await Promise.all(requestMap);
const items = responses.flatMap((res) => {
return res.data?.items || [];
});
return {
code: 0,
data: {
items,
},
msg: "success",
};
};
};
// /**
// * 批量获取用户信息
// * @param user_ids
// * @returns
// */
// const batchGet = async (
// user_ids: string[],
// user_id_type: "open_id" | "user_id",
// isSeek = false
// ) => {
// const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/batch`;
// const headers = isSeek
// ? {
// Authorization: `Bearer ${await db.tenantAccessToken.getSeek()}`,
// }
// : {};
// // 如果user_id长度超出50需要分批请求
// const user_idsLen = user_ids.length;
// const maxLen = 50;
// const requestMap = Array.from(
// { length: Math.ceil(user_idsLen / maxLen) },
// (_, index) => {
// const start = index * maxLen;
// const user_idsSlice = user_ids.slice(start, start + maxLen);
// const getParams = `${user_idsSlice
// .map((id) => `user_ids=${id}`)
// .join("&")}&user_id_type=${user_id_type}`;
// return larkNetTool.get<LarkServer.BatchUserInfoRes>(
// URL,
// getParams,
// headers
// );
// }
// );
// const responses = await Promise.all(requestMap);
// const items = responses.flatMap((res) => {
// return res.data?.items || [];
// });
// return {
// code: 0,
// data: {
// items,
// },
// msg: "success",
// };
// };
const user = {
code2Login,

View File

@ -1,5 +1,5 @@
const localUrl = "http://localhost:3000/micro_app/seek/batch_user";
const prodUrl = "https://egg.imoaix.cn/micro_app/seek/batch_user";
const localUrl = "http://localhost:3000/micro_app/batch_user";
const prodUrl = "https://egg.imoaix.cn/micro_app/batch_user";
const res = await fetch(localUrl, {
method: "POST",
@ -7,9 +7,9 @@ const res = await fetch(localUrl, {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_ids: ["zhaoyingbo", "libo12"],
user_ids: ["zhaoyingbo"],
user_id_type: "user_id",
}),
});
console.log(await res.json());
console.log(JSON.stringify(await res.json()));

View File

@ -18,4 +18,10 @@ export namespace DB {
union_id?: string[];
user_id?: string[];
}
export type AppName = "egg" | "seek" | "phone";
export enum AppNameEnum {
egg = "egg",
seek = "seek",
phone = "phone",
}
}

View File

@ -1,9 +1,11 @@
import { DB } from "./db";
import { LarkServer } from "./larkServer";
export namespace MsgProxy {
export interface BaseBody {
msg_type: LarkServer.MsgType;
content: string;
app_name?: DB.AppName;
}
export interface GroupBody extends BaseBody {
group_id: string;

4
utils/pathTools.ts Normal file
View File

@ -0,0 +1,4 @@
// 裁剪指定prefix路径
export function trimPathPrefix(path: string, prefix: string): string {
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
}