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 = { 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 = { code?: number | string; msg?: string | null; data?: T; }; type RequestOptions = { method?: "GET" | "POST"; body?: Record; 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 { return organizationRequest(organizationPath("ZHINIAN_ORG_LIST_PATH", "/organization/organizationList"), { method: "GET", accessToken: context.accessToken }); } export async function listOrganizationGroups(organizationId: string, context: OrganizationRequestContext = {}): Promise { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(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> { const path = organizationPath("ZHINIAN_ORG_MEMBER_LIST_PATH", "/adminOrganization/organizationMember/organizationMemberList"); return organizationRequest>(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 { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(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 { return organizationRequest(organizationPath("ZHINIAN_ORG_MEMBER_REMOVE_PATH", "/organizationMember/removeOrganizationMember"), { method: "POST", body: compactBody({ memberId }), accessToken: context.accessToken }); } export function emptyPage(pageNum = 1, pageSize = 10): PageResult { 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(path: string, options: RequestOptions = {}): Promise { 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>(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 = { 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(response: Response): Promise { 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): Record { 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() || ""; }