412 lines
14 KiB
TypeScript
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() || "";
|
|
}
|