Files
----/前端源码/uni-app/utils/poll-test-solution-detail.ts

278 lines
8.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('详情生成超时,请稍后在「我的方案」中查看');
}