upload project source code
This commit is contained in:
277
前端源码/uni-app/utils/poll-test-solution-detail.ts
Normal file
277
前端源码/uni-app/utils/poll-test-solution-detail.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
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('详情生成超时,请稍后在「我的方案」中查看');
|
||||
}
|
||||
Reference in New Issue
Block a user