Files
NianAIGC/lib/server/organization-client.ts
2026-07-03 11:25:25 +08:00

412 lines
14 KiB
TypeScript

export type OrganizationInfo = {
organizationId: string;
organizationName: string;
organizationType?: string;
organizationBindTenantId?: number;
};
export type OrganizationGroupInfo = {
groupId: string;
groupName: string;
groupDesc?: string;
};
export type OrganizationRoleInfo = {
roleId: string;
roleName: string;
};
export type OrganizationMemberInfo = {
memberId: string;
memberName: string;
memberStatus: "0" | "1" | "2" | string;
memberPhone: string;
organizationId: string;
memberRoleId?: string;
memberRoleName?: string;
memberGroupId?: string;
memberGroupName?: string;
memberUserId?: string;
};
export type PlatformUserInfo = {
userId: string;
username?: string;
phone: string;
tenantId: number;
name?: string;
nickname?: string;
mustChangePassword?: boolean;
lockFlag?: string;
initialPassword?: string | null;
};
export type PageResult<T> = {
records: T[];
total: number;
size: number;
current: number;
pages: number;
};
export type OrganizationApiConfig = {
organizationBaseUrl: string;
staffBaseUrl: string;
organizationToken: string;
staffToken: string;
tenantId: string;
defaultOrganizationId: string;
configured: boolean;
missing: string[];
staffConfigured: boolean;
staffMissing: string[];
};
export type OrganizationRequestContext = {
accessToken?: string;
};
type OrganizationApiResponse<T> = {
code?: number | string;
msg?: string | null;
data?: T;
};
type RequestOptions = {
method?: "GET" | "POST";
body?: Record<string, unknown>;
inner?: boolean;
service?: "organization" | "staff";
accessToken?: string;
};
export class OrganizationApiError extends Error {
status: number;
details?: unknown;
path?: string;
constructor(message: string, status = 502, details?: unknown, path?: string) {
super(message);
this.name = "OrganizationApiError";
this.status = status;
this.details = details;
this.path = path;
}
}
export function getOrganizationApiConfig(accessToken?: string): OrganizationApiConfig {
const organizationBaseUrl = cleanEnv(process.env.ZHINIAN_ORG_API_BASE_URL);
const staffBaseUrl = cleanEnv(process.env.ZHINIAN_STAFF_API_BASE_URL);
const loginToken = cleanEnv(accessToken);
const organizationToken = loginToken || cleanEnv(process.env.ZHINIAN_ORG_API_TOKEN);
const staffToken = cleanEnv(process.env.ZHINIAN_STAFF_API_TOKEN) || organizationToken;
const tenantId = cleanEnv(process.env.ZHINIAN_ORG_TENANT_ID);
const defaultOrganizationId = cleanEnv(process.env.ZHINIAN_ORG_ID);
const missing: string[] = [];
if (!organizationBaseUrl) missing.push("ZHINIAN_ORG_API_BASE_URL");
if (!organizationToken) missing.push("当前登录token或ZHINIAN_ORG_API_TOKEN");
const staffMissing: string[] = [];
if (!staffBaseUrl) staffMissing.push("ZHINIAN_STAFF_API_BASE_URL");
if (!staffToken) staffMissing.push("当前登录token或ZHINIAN_STAFF_API_TOKEN");
if (!tenantId) staffMissing.push("ZHINIAN_ORG_TENANT_ID");
return {
organizationBaseUrl,
staffBaseUrl,
organizationToken,
staffToken,
tenantId,
defaultOrganizationId,
configured: missing.length === 0,
missing,
staffConfigured: staffMissing.length === 0,
staffMissing
};
}
export function organizationApiConfigured(): boolean {
return getOrganizationApiConfig().configured;
}
export function accountManagementConfigured(): boolean {
const config = getOrganizationApiConfig("login-token");
return config.configured && config.staffConfigured;
}
export async function listOrganizations(context: OrganizationRequestContext = {}): Promise<OrganizationInfo[]> {
return organizationRequest<OrganizationInfo[]>(organizationPath("ZHINIAN_ORG_LIST_PATH", "/organization/organizationList"), {
method: "GET",
accessToken: context.accessToken
});
}
export async function listOrganizationGroups(organizationId: string, context: OrganizationRequestContext = {}): Promise<OrganizationGroupInfo[]> {
return organizationRequest<OrganizationGroupInfo[]>(organizationPath("ZHINIAN_ORG_GROUP_LIST_PATH", "/organizationGroup/organizationGroupList"), {
method: "POST",
body: compactBody({ organizationId }),
accessToken: context.accessToken
});
}
export async function createOrganizationGroup(input: {
organizationId: string;
groupName: string;
groupDesc?: string;
parentId?: string;
}, context: OrganizationRequestContext = {}): Promise<boolean> {
return organizationRequest<boolean>(organizationPath("ZHINIAN_ORG_GROUP_CREATE_PATH", "/organizationGroup/createOrganizationGroup"), {
method: "POST",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function listOrganizationRoles(organizationId: string, context: OrganizationRequestContext = {}): Promise<OrganizationRoleInfo[]> {
return organizationRequest<OrganizationRoleInfo[]>(organizationPath("ZHINIAN_ORG_ROLE_LIST_PATH", "/organizationRole/organizationRoleList"), {
method: "POST",
body: compactBody({ organizationId }),
accessToken: context.accessToken
});
}
export async function listOrganizationMembers(input: {
organizationId: string;
pageNum: number;
pageSize: number;
groupIdList?: string[];
memberName?: string;
memberStatus?: string;
}, context: OrganizationRequestContext = {}): Promise<PageResult<OrganizationMemberInfo>> {
const path = organizationPath("ZHINIAN_ORG_MEMBER_LIST_PATH", "/adminOrganization/organizationMember/organizationMemberList");
return organizationRequest<PageResult<OrganizationMemberInfo>>(path, {
method: "POST",
inner: organizationMemberListPathNeedsInner(path),
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function addOrganizationMember(input: {
memberName: string;
memberPhone: string;
roleId: string;
organizationId: string;
groupId: string;
}, context: OrganizationRequestContext = {}): Promise<boolean> {
return organizationRequest<boolean>(organizationPath("ZHINIAN_ORG_MEMBER_ADD_PATH", "/organizationMember/addOrganizationMember"), {
method: "POST",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function createPlatformUser(input: {
tenantId: number;
username?: string;
phone: string;
name?: string;
nickname?: string;
initialPassword?: string;
mustChangePassword?: boolean;
}, context: OrganizationRequestContext = {}): Promise<PlatformUserInfo> {
return organizationRequest<PlatformUserInfo>(organizationPath("ZHINIAN_STAFF_USER_CREATE_PATH", "/adminPcUser/createPlatformUser"), {
method: "POST",
service: "staff",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function getPlatformUserByPhone(input: {
tenantId: number;
phone: string;
}, context: OrganizationRequestContext = {}): Promise<PlatformUserInfo | null> {
return organizationRequest<PlatformUserInfo | null>(organizationPath("ZHINIAN_STAFF_USER_BY_PHONE_PATH", "/adminPcUser/getPlatformUserByPhone"), {
method: "POST",
service: "staff",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function resetPlatformUserPassword(input: {
tenantId: number;
userId: number;
newPassword: string;
mustChangePassword?: boolean;
}, context: OrganizationRequestContext = {}): Promise<boolean> {
return organizationRequest<boolean>(organizationPath("ZHINIAN_STAFF_PASSWORD_RESET_PATH", "/adminPcUser/resetPlatformUserPassword"), {
method: "POST",
service: "staff",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function addOrganizationMemberAndCreatePlatformUser(input: {
tenantId: number;
username?: string;
phone?: string;
name?: string;
nickname?: string;
initialPassword?: string;
mustChangePassword?: boolean;
memberName?: string;
memberPhone?: string;
roleId: string;
organizationId: string;
groupId: string;
}, context: OrganizationRequestContext = {}): Promise<PlatformUserInfo> {
return organizationRequest<PlatformUserInfo>(organizationPath(
"ZHINIAN_STAFF_MEMBER_CREATE_PATH",
"/adminOrganization/organizationMember/addOrganizationMemberAndCreatePlatformUser"
), {
method: "POST",
service: "staff",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function updateOrganizationMember(input: {
memberId: string;
memberName?: string;
roleId?: string;
groupId?: string;
}, context: OrganizationRequestContext = {}): Promise<boolean> {
return organizationRequest<boolean>(organizationPath("ZHINIAN_ORG_MEMBER_UPDATE_PATH", "/organizationMember/updateOrganizationMember"), {
method: "POST",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function modifyOrganizationMemberStatus(input: {
memberId: string;
memberStatus: "0" | "1" | "2";
}, context: OrganizationRequestContext = {}): Promise<boolean> {
return organizationRequest<boolean>(organizationPath("ZHINIAN_ORG_MEMBER_STATUS_PATH", "/organizationMember/modifyOrganizationMemberStatus"), {
method: "POST",
body: compactBody(input),
accessToken: context.accessToken
});
}
export async function removeOrganizationMember(memberId: string, context: OrganizationRequestContext = {}): Promise<boolean> {
return organizationRequest<boolean>(organizationPath("ZHINIAN_ORG_MEMBER_REMOVE_PATH", "/organizationMember/removeOrganizationMember"), {
method: "POST",
body: compactBody({ memberId }),
accessToken: context.accessToken
});
}
export function emptyPage<T>(pageNum = 1, pageSize = 10): PageResult<T> {
return {
records: [],
total: 0,
size: pageSize,
current: pageNum,
pages: 0
};
}
export function isOrganizationRouteNotFound(error: unknown): error is OrganizationApiError {
if (!(error instanceof OrganizationApiError) || error.status !== 404) return false;
const detailMessage = typeof (error.details as { msg?: unknown } | undefined)?.msg === "string"
? String((error.details as { msg?: string }).msg)
: "";
return /No static resource/i.test(detailMessage) ||
/请求路径不匹配|没有暴露|未在当前组织服务暴露/i.test(error.message);
}
export function isOrganizationPermissionDenied(error: unknown): error is OrganizationApiError {
if (!(error instanceof OrganizationApiError)) return false;
const detailMessage = typeof (error.details as { msg?: unknown } | undefined)?.msg === "string"
? String((error.details as { msg?: string }).msg)
: "";
return /仅管理员角色允许调用|缺少.*管理员|管理员角色/.test(`${error.message} ${detailMessage}`);
}
async function organizationRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
const config = getOrganizationApiConfig(options.accessToken);
const missing = options.service === "staff" ? config.staffMissing : config.missing;
const configured = options.service === "staff" ? config.staffConfigured : config.configured;
if (!configured) {
throw new OrganizationApiError(`组织账号接口配置不完整:${missing.join(", ")}`, 503);
}
const method = options.method || "POST";
const baseUrl = options.service === "staff" ? config.staffBaseUrl : config.organizationBaseUrl;
const token = options.service === "staff" ? config.staffToken : config.organizationToken;
const response = await fetch(buildOrganizationUrl(baseUrl, path), {
method,
headers: requestHeaders(config, token, method, options.inner),
body: method === "GET" ? undefined : JSON.stringify(options.body || {}),
cache: "no-store"
});
const payload = await parsePayload<OrganizationApiResponse<T>>(response);
if (!response.ok) {
throw new OrganizationApiError(organizationErrorMessage(payload?.msg, path, response.status), response.status, payload, path);
}
if (payload && payload.code !== undefined && String(payload.code) !== "0") {
throw new OrganizationApiError(organizationErrorMessage(payload.msg, path, 502), 502, payload, path);
}
return payload?.data as T;
}
function organizationErrorMessage(message: string | null | undefined, path: string, status: number): string {
const text = message || `组织接口请求失败:${status}`;
if (/No static resource/i.test(text) && path.includes("/organizationMember/organizationMemberList")) {
return `成员列表查询暂未开放。当前 hotelStaff 没有暴露 ${path},账号创建和密码维护仍可用;请让运维提供成员查询外部路径后配置 ZHINIAN_ORG_MEMBER_LIST_PATH。`;
}
if (/No static resource/i.test(text) && path.startsWith("/organization")) {
return `组织服务地址或请求路径不匹配:当前地址没有暴露 ${path},请确认 ZHINIAN_ORG_API_BASE_URL、路径覆盖配置和统一服务路由。`;
}
return text;
}
function requestHeaders(config: OrganizationApiConfig, token: string, method: string, inner?: boolean): HeadersInit {
const headers: Record<string, string> = {
Accept: "application/json"
};
if (method !== "GET") headers["Content-Type"] = "application/json; charset=utf-8";
headers.Authorization = /^Bearer\s+/i.test(token) ? token : `Bearer ${token}`;
if (config.tenantId) headers.tenantId = config.tenantId;
if (inner) headers.from = "Y";
return headers;
}
function buildOrganizationUrl(baseUrl: string, path: string): string {
return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
}
function organizationPath(envName: string, fallback: string): string {
return cleanEnv(process.env[envName]) || fallback;
}
function organizationMemberListPathNeedsInner(path: string): boolean {
return /^\/?organizationMember\//.test(path);
}
async function parsePayload<T>(response: Response): Promise<T | null> {
const text = await response.text();
if (!text) return null;
try {
return JSON.parse(text) as T;
} catch {
throw new OrganizationApiError("组织接口返回了非 JSON 响应。", response.ok ? 502 : response.status, text.slice(0, 500));
}
}
function compactBody(input: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(input).filter(([, value]) => {
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim().length > 0;
if (Array.isArray(value)) return value.length > 0;
return true;
}));
}
function cleanEnv(value: string | undefined): string {
return value?.trim() || "";
}