278 lines
8.5 KiB
TypeScript
278 lines
8.5 KiB
TypeScript
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<number | null> {
|
||
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<number> {
|
||
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<void> {
|
||
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<any> {
|
||
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<any> {
|
||
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('详情生成超时,请稍后在「我的方案」中查看');
|
||
}
|