import { namingApi } from '@/api/naming'; export function parseMaybeJson(value: any): any { if (value == null) return value; if (typeof value === 'object') return value; if (typeof value !== 'string') return value; const raw = value.trim(); if (!raw) return value; try { return JSON.parse(raw); } catch { return value; } } /** 公司测名详解 JSON 形态(与个人测名部分字段重名,需优先识别) */ export function isCompanyTestDetailShape(data: any): boolean { const d = parseMaybeJson(data); if (!d || typeof d !== 'object') return false; return ( (!!d.businessPattern && !!d.characterAnalysis) || (!!d.header && !!d.team && !!d.years && !!d.execution) ); } /** * 与个人测名详解页一致:header + 任一「个人专用」模块。 * 注意:公司测名也会带 liuyao / zodiac_sign / lucky_* 等,不能用这些单独判定个人,否则会误判。 */ export function isPersonalTestDetailReady(data: any): boolean { const d = parseMaybeJson(data); if (!d || typeof d !== 'object' || !d.header) return false; if (isCompanyTestDetailShape(d)) return false; const hasPersonalCore = !!( d.meaning_and_zodiac || d.strokes_wuge_sancai || d.six_dimension || d.master_message || d.phonetics || d.bazi_name_fit || d.name_popularity ); if (hasPersonalCore) return true; return !!( d.liuyao || d.wuxing_bagua || d.zodiac_sign || d.career_plan || d.lucky_numbers || d.lucky_colors ); } /** 与公司测名详解页一致 */ export function isCompanyTestDetailReady(data: any): boolean { const d = parseMaybeJson(data); if (!d || typeof d !== 'object') return false; return isCompanyTestDetailShape(d); } export type LiveTestMode = 'personal' | 'company'; /** * 与 index.vue handleMyPlanDetail 分流逻辑对齐(测名场景) */ export function classifyLiveTestDetail( detail: any, mode: LiveTestMode, ): { kind: 'personal' | 'company'; showBusinessFortune: boolean } { const d = parseMaybeJson(detail); const st = String(d?.service_type || '').trim().toLowerCase(); const isNamingOrRenamingReport = st === 'naming' || st === 'renaming' || st === 'company_naming' || st === 'company_renaming'; const isCompanyTestByShape = isCompanyTestDetailShape(d); const isCompanyTestByServiceType = st === 'test'; const showBusinessFortune = !(isCompanyTestByServiceType || isCompanyTestByShape); // 必须先分流公司测名:公司与个人详情可能共用 header,且公司也常有 liuyao / 属相 / 幸运色等 if (mode === 'company' || isCompanyTestByShape) { return { kind: 'company', showBusinessFortune }; } const isTestNameDetailData = !!d && typeof d === 'object' && !!d.header && !!( d.meaning_and_zodiac || d.strokes_wuge_sancai || d.six_dimension || d.master_message || d.phonetics || d.bazi_name_fit || d.name_popularity || d.liuyao || d.wuxing_bagua || d.zodiac_sign || d.career_plan || d.lucky_numbers || d.lucky_colors ); if (!isNamingOrRenamingReport && isTestNameDetailData) { return { kind: 'personal', showBusinessFortune: true }; } return { kind: 'personal', showBusinessFortune: true }; } /** 兼容 axios 未解包或嵌套 data 的测名返回 */ export function normalizeScoringPayload(res: any): any { if (!res || typeof res !== 'object') return res; const d = (res as any).data; if (d && typeof d === 'object') { if ( d.report_id != null || d.reportId != null || d.solution_id != null || d.solutionId != null || (Array.isArray(d.solutions) && d.solutions.length > 0) ) { return d; } } return res; } export async function extractSolutionIdFromScoring(res: any): Promise { res = normalizeScoringPayload(res); if (!res || typeof res !== 'object') return null; const direct = (res as any).solution_id ?? (res as any).solutionId; if (direct != null) { const n = Number(direct); if (Number.isFinite(n) && n > 0) return n; } const sols = (res as any).solutions; if (Array.isArray(sols) && sols[0]) { const id = sols[0].id ?? sols[0].solution_id; if (id != null) { const n = Number(id); if (Number.isFinite(n) && n > 0) return n; } } const ridRaw = (res as any).report_id ?? (res as any).reportId ?? ((res as any).report && typeof (res as any).report === 'object' ? (res as any).report.id : null); if (ridRaw == null || ridRaw === '') return null; const ridNum = Number(ridRaw); if (!Number.isFinite(ridNum) || ridNum <= 0) return null; try { const solutionsResult = await namingApi.getSolutionsByReportId(ridNum); const solutions = solutionsResult?.solutions || solutionsResult?.items || solutionsResult; if (Array.isArray(solutions) && solutions[0]) { const id = solutions[0].id ?? solutions[0].solution_id; if (id != null) { const n = Number(id); if (Number.isFinite(n) && n > 0) return n; } } } catch { return null; } return null; } /** * 电脑端测名:方案可能尚未落库,按间隔重复解析 solution_id / 拉取报告下方案列表, * 在成功前不结束 loading、不提示「未获取到方案编号」。 */ export async function pollUntilSolutionIdFromScoring( scoringRes: any, options?: { intervalMs?: number; maxAttempts?: number; signal?: AbortSignal }, ): Promise { const intervalMs = options?.intervalMs ?? 5000; const maxAttempts = options?.maxAttempts ?? 120; const signal = options?.signal; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (signal?.aborted) { throw new Error('aborted'); } const id = await extractSolutionIdFromScoring(scoringRes); if (id) return id; if (signal?.aborted) { throw new Error('aborted'); } await sleep(intervalMs); } throw new Error('详情生成超时,请稍后在「我的方案」中查看'); } function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } /** * 首次立即请求 full_detail,未就绪则每隔 intervalMs 再请求,与电脑端测名 loading 配合使用。 */ export async function pollTestSolutionDetail( solutionId: number, mode: LiveTestMode, options?: { intervalMs?: number; maxAttempts?: number; signal?: AbortSignal }, ): Promise { const intervalMs = options?.intervalMs ?? 5000; const maxAttempts = options?.maxAttempts ?? 120; const signal = options?.signal; const isReady = mode === 'personal' ? isPersonalTestDetailReady : isCompanyTestDetailReady; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (signal?.aborted) { throw new Error('aborted'); } let detail: any; try { detail = await namingApi.getSolutionDetail(solutionId); } catch { await sleep(intervalMs); continue; } const parsed = parseMaybeJson(detail); if (isReady(parsed)) { return parsed; } if (signal?.aborted) { throw new Error('aborted'); } await sleep(intervalMs); } throw new Error('详情生成超时,请稍后在「我的方案」中查看'); } /** * 起名/改名通用轮询:只要 full_detail 返回可用对象即视为就绪。 * 适用于非测名报告(起名、改名)详情。 */ export async function pollSolutionDetailUntilReady( solutionId: number, options?: { intervalMs?: number; maxAttempts?: number; signal?: AbortSignal }, ): Promise { const intervalMs = options?.intervalMs ?? 5000; const maxAttempts = options?.maxAttempts ?? 120; const signal = options?.signal; const isUsableObject = (v: any): boolean => { if (!v || typeof v !== 'object') return false; const keys = Object.keys(v); if (keys.length === 0) return false; // 兼容后端「处理中」文案 const msg = String((v as any).msg || (v as any).message || '').toLowerCase(); if (msg.includes('处理中') || msg.includes('processing') || msg.includes('pending')) return false; return true; }; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (signal?.aborted) throw new Error('aborted'); try { const detail = await namingApi.getSolutionDetail(solutionId); const parsed = parseMaybeJson(detail); if (isUsableObject(parsed)) return parsed; } catch { // ignore and retry } if (signal?.aborted) throw new Error('aborted'); await sleep(intervalMs); } throw new Error('详情生成超时,请稍后在「我的方案」中查看'); }