upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View 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('详情生成超时,请稍后在「我的方案」中查看');
}