import { ContentType, FieldPosition } from '@/constants/itfxEnums'; /** 参数树节点种类:0=普通,1=Mapping Body(仅 FormUrlEncoded 映射体) */ export const ParaKind = { Normal: 0, MappingBody: 1, } as const; export type InterfaceParameterMode = 'upstream' | 'downstream'; /** 字典 / 表单可能返回 string,统一为数字再比较 */ export function normalizeContentType(contentType: unknown): number | undefined { if (contentType === '' || contentType === undefined || contentType === null) { return undefined; } const n = Number(contentType); return Number.isNaN(n) ? undefined : n; } export function isNoneContentType(contentType?: unknown): boolean { return normalizeContentType(contentType) === ContentType.None; } export function isFormUrlEncodedContentType(contentType?: unknown): boolean { return normalizeContentType(contentType) === ContentType.FormUrlEncoded; } export function isJsonOrXmlContentType(contentType?: unknown): boolean { const ct = normalizeContentType(contentType); return ct === ContentType.Json || ct === ContentType.Xml; } /** FormUrlEncoded 与 none:需 Mapping Body Tab(Query 等可 $FromMappingBody) */ export function isMappingBodyContentType(contentType?: unknown): boolean { return isFormUrlEncodedContentType(contentType) || isNoneContentType(contentType); } /** 从 GetInterfaceById 响应拆成 InterfaceDetail 各 Tab 数据 */ export function hydrateDetailParamsFromInterface(apiData: Record) { const contentType = apiData.contentType as number | undefined; const isForm = isFormUrlEncodedContentType(contentType); const isNone = isNoneContentType(contentType); return { queryParams: apiData.queryFormParameterList ?? apiData.formParameterList ?? [], headerParams: apiData.headerFormParameterList ?? [], responseParams: apiData.outParameterList ?? [], requestParams: isForm || isNone ? [] : (apiData.requestBodyParameterList ?? apiData.inParameterList ?? []), bodyFormParams: isForm ? (apiData.bodyFormParameterList ?? []) : [], mappingBodyParams: isForm || isNone ? (apiData.mappingBodyParameterList ?? []) : [], }; } /** vxe 树表 fullData 常为带 children 的层级;转为扁平 id/pId 列表 */ export function flattenTreeTableRows(arr: any[]): any[] { if (!Array.isArray(arr) || !arr.length) return []; return arr.reduce((acc: any[], val: any) => { const { children, ...rest } = val; acc.push(rest); if (Array.isArray(children) && children.length) { acc.push(...flattenTreeTableRows(children)); } return acc; }, []); } function flattenTree(arr: any[]): any[] { return flattenTreeTableRows(arr); } /** 读取树表完整扁平数据(避免 fullData 仅返回根节点导致子级丢失) */ export function getTreeTableFlatRows(fromGrid?: any[], propsData?: any[]): any[] { const flatFromGrid = flattenTreeTableRows(fromGrid || []); const propsFlat = Array.isArray(propsData) ? propsData : []; if (flatFromGrid.length >= propsFlat.length) { return flatFromGrid.length ? flatFromGrid : [...propsFlat]; } return propsFlat.length ? [...propsFlat] : flatFromGrid; } function getRowSeq(row: any, tableRef: any): string { return `${tableRef?.xGrid?.getRowSeq(row)}` || '0'; } function normalizeRowId(row: any): number { const id = Number(row?.id ?? row?.Id); return Number.isNaN(id) ? 0 : id; } function buildParamPathKey(pId: number, name: string): string { return `${pId}:${String(name || '').trim().toLowerCase()}`; } type NormalizedParamRow = { id: number; pId: number; name: string; alias: string; type: number; isRequired: boolean; cnName: string | null; description: string | null; sort: string; }; function normalizeTreeParamRow(row: any): NormalizedParamRow { return { id: normalizeRowId(row), pId: Number(row?.pId ?? row?.PId ?? 0), name: String(row?.name ?? row?.Name ?? '').trim(), alias: String(row?.alias ?? row?.Alias ?? '').trim(), type: Number(row?.type ?? row?.Type ?? 1), isRequired: !!(row?.isRequired ?? row?.IsRequired), cnName: row?.cnName === '' ? null : (row?.cnName ?? row?.CnName ?? null), description: row?.description ?? row?.Description ?? null, sort: String(row?.sort ?? row?.Sort ?? ''), }; } function isSameTreeParamRow(row: any, baseline: NormalizedParamRow): boolean { const current = normalizeTreeParamRow(row); return ( current.name === baseline.name && current.pId === baseline.pId && current.type === baseline.type && current.isRequired === baseline.isRequired && (current.cnName ?? null) === (baseline.cnName ?? null) && (current.description ?? null) === (baseline.description ?? null) ); } export interface OriginalParamSnapshot { ids: Set; pathToId: Map; rowById: Map; } export interface OriginalParamIdSets { query: OriginalParamSnapshot; header: OriginalParamSnapshot; bodyForm: OriginalParamSnapshot; request: OriginalParamSnapshot; mappingBody: OriginalParamSnapshot; response: OriginalParamSnapshot; } export function createEmptyOriginalParamSnapshot(): OriginalParamSnapshot { return { ids: new Set(), pathToId: new Map(), rowById: new Map(), }; } export function createEmptyOriginalParamIds(): OriginalParamIdSets { const empty = createEmptyOriginalParamSnapshot(); return { query: empty, header: createEmptyOriginalParamSnapshot(), bodyForm: createEmptyOriginalParamSnapshot(), request: createEmptyOriginalParamSnapshot(), mappingBody: createEmptyOriginalParamSnapshot(), response: createEmptyOriginalParamSnapshot(), }; } /** 从加载时的参数行构建快照(id / 路径 / 原始字段) */ export function buildOriginalParamSnapshot(rows: any[]): OriginalParamSnapshot { const snapshot = createEmptyOriginalParamSnapshot(); flattenTreeTableRows(rows || []).forEach((raw) => { const row = normalizeTreeParamRow(raw); if (row.id <= 0 || !row.name) return; snapshot.ids.add(row.id); snapshot.rowById.set(row.id, row); snapshot.pathToId.set(buildParamPathKey(row.pId, row.name), row.id); }); return snapshot; } function buildOriginalFormSnapshot(rows: any[]): OriginalParamSnapshot { const snapshot = createEmptyOriginalParamSnapshot(); (rows || []).forEach((raw) => { const rest = stripFormKvMeta(raw); const id = normalizeRowId(rest); const name = String(rest.name || rest.Name || '').trim(); if (id <= 0 || !name) return; const row: NormalizedParamRow = { id, pId: 0, name, alias: String(rest.value || rest.Value || '').trim(), type: 0, isRequired: false, cnName: null, description: rest.description || rest.Description || null, sort: '', }; snapshot.ids.add(id); snapshot.rowById.set(id, row); snapshot.pathToId.set(buildParamPathKey(0, name), id); }); return snapshot; } /** @deprecated 使用 buildOriginalParamSnapshot */ export function collectParamIds(rows: any[]): Set { return buildOriginalParamSnapshot(rows).ids; } export function buildOriginalParamIdsFromHydrated(source: { queryParams?: any[]; headerParams?: any[]; bodyFormParams?: any[]; requestParams?: any[]; mappingBodyParams?: any[]; responseParams?: any[]; }): OriginalParamIdSets { return { query: buildOriginalFormSnapshot(source.queryParams || []), header: buildOriginalFormSnapshot(source.headerParams || []), bodyForm: buildOriginalFormSnapshot(source.bodyFormParams || []), request: buildOriginalParamSnapshot(source.requestParams || []), mappingBody: buildOriginalParamSnapshot(source.mappingBodyParams || []), response: buildOriginalParamSnapshot(source.responseParams || []), }; } function mapTreeRow(item: any, optionType: number, tableRef: any, paraKind: number, serverId?: number) { const id = serverId ?? normalizeRowId(item); const pId = Number(item?.pId ?? item?.PId ?? 0); const name = String(item?.name ?? item?.Name ?? '').trim(); return { optionType, id, pId, alias: optionType === 1 ? name : (item?.alias || item?.Alias || name || ''), name, cnName: item?.cnName === '' ? null : (item?.cnName ?? item?.CnName ?? null), type: item?.type ?? item?.Type ?? 1, isRequired: !!(item?.isRequired ?? item?.IsRequired), sort: getRowSeq(item, tableRef), description: item?.description ?? item?.Description ?? null, paraKind, }; } function mapTreeChangeRows( actionRows: { insertRecords?: any[]; removeRecords?: any[]; updateRecords?: any[] }, tableRef: any, paraKind: number, ) { const install = (actionRows.insertRecords || []).map((i: any) => { if (!i.name) return null; return mapTreeRow(i, 1, tableRef, paraKind); }).filter(Boolean); const remove: any[] = []; (actionRows.removeRecords || []).forEach((i: any) => { remove.push(mapTreeRow(i, 2, tableRef, paraKind)); if (i.children?.length) { flattenTree(i.children).forEach((child: any) => { remove.push(mapTreeRow(child, 2, tableRef, paraKind)); }); } }); const update = (actionRows.updateRecords || []).map((i: any) => { if (!i.name) return null; return mapTreeRow(i, 3, tableRef, paraKind); }).filter(Boolean); return [...install, ...remove, ...update]; } /** * 编辑保存时对齐后端 MaintainParametersAsync: * - 仅提交增/删/改增量 * - 未变更的已有参数不传(后端会自动补全 DB 节点) * - 假 id + 已存在路径 → 按服务端 id 走修改,避免与 DB 补全节点重复 */ function buildTreeParameterList( actionRows: { insertRecords?: any[]; removeRecords?: any[]; updateRecords?: any[] }, treeData: any[], tableRef: any, paraKind: number, isEditMode: boolean, original: OriginalParamSnapshot, ) { const changes = mapTreeChangeRows(actionRows, tableRef, paraKind); const changeById = new Map(changes.map((c) => [Number(c.id), c])); const removedIds = new Set( changes.filter((c) => c.optionType === 2).map((c) => Number(c.id)), ); if (!isEditMode) { if (changes.length > 0) return changes; return flattenTree(treeData) .filter((item) => item?.name && String(item.name).trim() !== '') .map((item) => mapTreeRow(item, 1, tableRef, paraKind)); } const result: any[] = []; changes.filter((c) => c.optionType === 2).forEach((c) => result.push(c)); flattenTree(treeData) .filter((item) => item?.name && String(item.name).trim() !== '') .forEach((item) => { const id = normalizeRowId(item); const pId = Number(item?.pId ?? item?.PId ?? 0); const name = String(item?.name ?? item?.Name ?? '').trim(); const pathKey = buildParamPathKey(pId, name); if (removedIds.has(id)) return; const tracked = changeById.get(id); if (tracked && tracked.optionType !== 2) { result.push(tracked); return; } const serverIdByPath = original.pathToId.get(pathKey); const serverId = original.ids.has(id) ? id : serverIdByPath; const baseline = serverId ? original.rowById.get(serverId) : undefined; if (serverId && baseline) { if (!isSameTreeParamRow(item, baseline)) { result.push(mapTreeRow(item, 3, tableRef, paraKind, serverId)); } return; } if (!serverId) { result.push(mapTreeRow(item, 1, tableRef, paraKind)); } }); return result; } /** 客户端校验:同一 pId 下参数名不可重复(对齐后端 ValidateParameter) */ export function validateTreeParameterPaths(rows: any[]): string | null { const seen = new Set(); for (const item of flattenTree(rows)) { const name = String(item?.name ?? item?.Name ?? '').trim(); if (!name) continue; const pId = Number(item?.pId ?? item?.PId ?? 0); const key = buildParamPathKey(pId, name); if (seen.has(key)) { return `同一层级下不能存在重复参数名(${name})`; } seen.add(key); } return null; } /** 编辑模式下无增量变更时不传该列表,避免空数组覆盖后端已有参数 */ function assignParameterList( payload: Record, key: string, list: any[], isEditMode: boolean, hadOriginalItems: boolean, ) { if (isEditMode && list.length === 0 && hadOriginalItems) { delete payload[key]; return; } payload[key] = list; } function mapFormKvRow(row: any, optionType: number, fieldPosition: number, serverId?: number) { return { id: serverId ?? normalizeRowId(row), name: row.name || row.Name || '', value: row.value || row.Value || '', description: row.description || row.Description || '', optionType, fieldPosition, }; } function stripFormKvMeta(item: any) { const { _X_ROW_CHILD, _X_ROW_KEY, children, Id, ...rest } = item || {}; return rest; } function isSameFormKvRow(row: any, baseline: NormalizedParamRow): boolean { const rest = stripFormKvMeta(row); return ( String(rest.name || rest.Name || '').trim() === baseline.name && String(rest.value || rest.Value || '').trim() === String(baseline.alias || '') && String(rest.description || rest.Description || '').trim() === String(baseline.description || '') ); } function buildFormKvParameterList( changes: any[], tableData: any[], fieldPosition: number, isEditMode: boolean, original: OriginalParamSnapshot, ) { const isValid = (p: any) => p.name && String(p.name).trim() !== '' && p.value && String(p.value).trim() !== ''; const changeById = new Map(changes.map((c) => [Number(c.id), c])); const removedIds = new Set( changes.filter((c) => c.optionType === 2).map((c) => Number(c.id)), ); if (!isEditMode) { if (changes.length) return changes; return (tableData || []) .map((item) => mapFormKvRow(stripFormKvMeta(item), 1, fieldPosition)) .filter(isValid); } const result: any[] = []; changes.filter((c) => c.optionType === 2).forEach((c) => result.push(c)); (tableData || []).forEach((item) => { const rest = stripFormKvMeta(item); const id = normalizeRowId(rest); const name = String(rest.name || rest.Name || '').trim(); const pathKey = buildParamPathKey(0, name); if (removedIds.has(id)) return; const tracked = changeById.get(id); if (tracked && tracked.optionType !== 2) { result.push(tracked); return; } const serverIdByPath = original.pathToId.get(pathKey); const serverId = original.ids.has(id) ? id : serverIdByPath; const baseline = serverId ? original.rowById.get(serverId) : undefined; if (serverId && baseline) { if (!isSameFormKvRow(rest, baseline)) { result.push(mapFormKvRow(rest, 3, fieldPosition, serverId)); } return; } if (!serverId) { result.push(mapFormKvRow(rest, 1, fieldPosition)); } }); return result.filter(isValid); } function mapFormKvChanges( actionRows: { insertRecords?: any[]; removeRecords?: any[]; updateRecords?: any[] }, fieldPosition: number, ) { const mapRow = (row: any, optionType: number) => ({ id: normalizeRowId(row), name: row.name || '', value: row.value || '', description: row.description || '', optionType, fieldPosition, }); const list = [ ...(actionRows.insertRecords || []).map((r) => mapRow(r, 1)), ...(actionRows.removeRecords || []).map((r) => mapRow(r, 2)), ...(actionRows.updateRecords || []).map((r) => mapRow(r, 3)), ]; return list.filter( (p) => p.name && String(p.name).trim() !== '' && p.value && String(p.value).trim() !== '', ); } export interface InterfaceDetailSnapshot { queryParams: any[]; headerParams: any[]; bodyFormParams: any[]; requestParams: any[]; mappingBodyParams: any[]; responseParams: any[]; queryActionRows: { insertRecords: any[]; removeRecords: any[]; updateRecords: any[] }; headerActionRows: { insertRecords: any[]; removeRecords: any[]; updateRecords: any[] }; bodyFormActionRows: { insertRecords: any[]; removeRecords: any[]; updateRecords: any[] }; requestActionRows: { insertRecords: any[]; removeRecords: any[]; updateRecords: any[] }; mappingBodyActionRows: { insertRecords: any[]; removeRecords: any[]; updateRecords: any[] }; responseActionRows: { insertRecords: any[]; removeRecords: any[]; updateRecords: any[] }; queryTableRef: any; headerTableRef: any; bodyFormTableRef: any; requestTableRef: any; mappingBodyTableRef: any; responseTableRef: any; requestExample: string; originalParamIds?: OriginalParamIdSets; } export interface BuildSavePayloadOptions { formData: Record; mode: InterfaceParameterMode; isEditMode: boolean; snapshot: InterfaceDetailSnapshot; extraFields?: Record; } function snapshotSize(snapshot: OriginalParamSnapshot): boolean { return snapshot.ids.size > 0; } export function buildInterfaceSavePayload(options: BuildSavePayloadOptions) { const { formData, mode, isEditMode, snapshot, extraFields = {} } = options; const contentType = formData.contentType as number | undefined; const original = snapshot.originalParamIds ?? createEmptyOriginalParamIds(); const queryChanges = mapFormKvChanges(snapshot.queryActionRows, FieldPosition.Query); const headerChanges = mapFormKvChanges(snapshot.headerActionRows, FieldPosition.Header); const bodyFormChanges = mapFormKvChanges(snapshot.bodyFormActionRows, FieldPosition.Body); const queryFormParameterList = buildFormKvParameterList( queryChanges, snapshot.queryParams, FieldPosition.Query, isEditMode, original.query, ); const headerFormParameterList = buildFormKvParameterList( headerChanges, snapshot.headerParams, FieldPosition.Header, isEditMode, original.header, ); const bodyFormParameterList = buildFormKvParameterList( bodyFormChanges, snapshot.bodyFormParams, FieldPosition.Body, isEditMode, original.bodyForm, ); const outParameterList = buildTreeParameterList( snapshot.responseActionRows, snapshot.responseParams, snapshot.responseTableRef, ParaKind.Normal, isEditMode, original.response, ).filter((p) => p.name && String(p.name).trim() !== ''); const emptyLists = { inParameterList: [] as any[], requestBodyParameterList: [] as any[], mappingBodyParameterList: [] as any[], bodyFormParameterList: [] as any[], }; let payload: Record = { ...formData, ...extraFields, inParams: snapshot.requestExample, ...emptyLists, }; assignParameterList(payload, 'queryFormParameterList', queryFormParameterList, isEditMode, snapshotSize(original.query)); assignParameterList(payload, 'headerFormParameterList', headerFormParameterList, isEditMode, snapshotSize(original.header)); assignParameterList(payload, 'outParameterList', outParameterList, isEditMode, snapshotSize(original.response)); if (mode === 'upstream') { const inParameterList = buildTreeParameterList( snapshot.requestActionRows, snapshot.requestParams, snapshot.requestTableRef, ParaKind.Normal, isEditMode, original.request, ).filter((p) => p.name && String(p.name).trim() !== ''); assignParameterList(payload, 'inParameterList', inParameterList, isEditMode, snapshotSize(original.request)); } else if (isFormUrlEncodedContentType(contentType)) { assignParameterList(payload, 'bodyFormParameterList', bodyFormParameterList, isEditMode, snapshotSize(original.bodyForm)); const mappingBodyParameterList = buildTreeParameterList( snapshot.mappingBodyActionRows, snapshot.mappingBodyParams, snapshot.mappingBodyTableRef, ParaKind.MappingBody, isEditMode, original.mappingBody, ).filter((p) => p.name && String(p.name).trim() !== ''); assignParameterList(payload, 'mappingBodyParameterList', mappingBodyParameterList, isEditMode, snapshotSize(original.mappingBody)); } else if (isNoneContentType(contentType)) { const mappingBodyParameterList = buildTreeParameterList( snapshot.mappingBodyActionRows, snapshot.mappingBodyParams, snapshot.mappingBodyTableRef, ParaKind.MappingBody, isEditMode, original.mappingBody, ).filter((p) => p.name && String(p.name).trim() !== ''); assignParameterList(payload, 'mappingBodyParameterList', mappingBodyParameterList, isEditMode, snapshotSize(original.mappingBody)); } else if (isJsonOrXmlContentType(contentType)) { const requestBodyParameterList = buildTreeParameterList( snapshot.requestActionRows, snapshot.requestParams, snapshot.requestTableRef, ParaKind.Normal, isEditMode, original.request, ).filter((p) => p.name && String(p.name).trim() !== ''); assignParameterList(payload, 'requestBodyParameterList', requestBodyParameterList, isEditMode, snapshotSize(original.request)); } if (isEditMode) { payload.optionType = 3; } else { delete payload.id; payload.optionType = 1; } return payload; } export function validateInterfaceSavePayload( payload: Record, mode: InterfaceParameterMode, ): string | null { const hasItems = (list?: any[]) => Array.isArray(list) && list.length > 0; const mustEmpty = (list?: any[]) => !list || list.length === 0; const isEdit = payload.optionType === 3; const treeLists: any[][] = []; if (mode === 'upstream') treeLists.push(payload.inParameterList || []); else if (isFormUrlEncodedContentType(payload.contentType)) { treeLists.push(payload.mappingBodyParameterList || []); } else if (isNoneContentType(payload.contentType)) { treeLists.push(payload.mappingBodyParameterList || []); } else if (isJsonOrXmlContentType(payload.contentType)) { treeLists.push(payload.requestBodyParameterList || []); } treeLists.push(payload.outParameterList || []); for (const list of treeLists) { const pathError = validateTreeParameterPaths(list); if (pathError) return pathError; } if (mode === 'upstream') { if (!isEdit && !hasItems(payload.inParameterList)) return '上游入参(In Parameters)不能为空'; if (!mustEmpty(payload.requestBodyParameterList)) return '上游不应包含 requestBodyParameterList'; if (!mustEmpty(payload.mappingBodyParameterList)) return '上游不应包含 mappingBodyParameterList'; if (!mustEmpty(payload.bodyFormParameterList)) return '上游不应包含 bodyFormParameterList'; return null; } const ct = payload.contentType; if (isNoneContentType(ct)) { if (!mustEmpty(payload.requestBodyParameterList)) return 'none 不应包含 requestBodyParameterList'; if (!mustEmpty(payload.bodyFormParameterList)) return 'none 不应包含 bodyFormParameterList'; if (!mustEmpty(payload.inParameterList)) return 'none 不应包含 inParameterList'; return null; } if (isFormUrlEncodedContentType(ct)) { if (!isEdit && !hasItems(payload.bodyFormParameterList)) return 'FormUrlEncoded 须配置 Body 表单字段'; if (!isEdit && !hasItems(payload.mappingBodyParameterList)) return 'FormUrlEncoded 须配置 Mapping Body'; if (!mustEmpty(payload.inParameterList)) return '下游 FormUrlEncoded 不应包含 inParameterList'; if (!mustEmpty(payload.requestBodyParameterList)) return '下游 FormUrlEncoded 不应包含 requestBodyParameterList'; return null; } if (isJsonOrXmlContentType(ct)) { if (!isEdit && !hasItems(payload.requestBodyParameterList)) return 'Json/Xml 下游须配置 Body 参数树'; if (!mustEmpty(payload.inParameterList)) return '下游 Json/Xml 不应包含 inParameterList'; if (!mustEmpty(payload.mappingBodyParameterList)) return '下游 Json/Xml 不应包含 mappingBodyParameterList'; if (!mustEmpty(payload.bodyFormParameterList)) return '下游 Json/Xml 不应包含 bodyFormParameterList'; return null; } return '不支持的 ContentType'; } /** 映射页:下游目标参数树来源 */ export function resolveDownstreamMappingTargetList(interfaceData: Record): any[] { const ct = interfaceData.contentType; if (isNoneContentType(ct)) { return interfaceData.mappingBodyParameterList ?? []; } if (isFormUrlEncodedContentType(ct)) { return interfaceData.mappingBodyParameterList ?? []; } if (isJsonOrXmlContentType(ct)) { return interfaceData.requestBodyParameterList ?? interfaceData.inParameterList ?? []; } return interfaceData.requestBodyParameterList ?? interfaceData.inParameterList ?? []; }