feat: refactor AI interpretation system and fix recordId issues

- Refactored AI interpretation table to use proper 1-to-1 relationship with reading records
- Fixed recordId parameter passing in AnalysisResultDisplay component
- Updated database schema to use reading_id instead of analysis_id
- Removed complex string ID generation logic
- Fixed TypeScript type definitions for all ID fields
- Added database migration scripts for AI interpretation refactoring
- Improved error handling and debugging capabilities
This commit is contained in:
patdelphi
2025-08-23 23:05:13 +08:00
parent 529ae3b8aa
commit d1713be5f5
25 changed files with 1580 additions and 264 deletions

View File

@@ -451,13 +451,14 @@ const AnalysisResultDisplay: React.FC<AnalysisResultDisplayProps> = ({
userId={userId}
divinationMethod={divinationMethod}
analysisData={preAnalysisData}
recordId={recordId}
/>
);
}
// 对于紫微斗数,如果有 birthDate 参数,直接返回 CompleteZiweiAnalysis 组件(不添加额外容器)
if (analysisType === 'ziwei' && birthDate) {
return <CompleteZiweiAnalysis birthDate={birthDate} analysisData={preAnalysisData} />;
return <CompleteZiweiAnalysis birthDate={birthDate} analysisData={preAnalysisData} recordId={recordId} />;
}
// 如果没有分析结果数据

View File

@@ -287,7 +287,7 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
<AIInterpretationButton
analysisData={analysisData}
analysisType="bazi"
analysisId={recordId?.toString()}
recordId={recordId}
onConfigClick={() => setShowAIConfig(true)}
/>
</div>

View File

@@ -276,7 +276,7 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
<AIInterpretationButton
analysisData={analysisData}
analysisType="yijing"
analysisId={recordId?.toString()}
recordId={recordId}
onConfigClick={() => setShowAIConfig(true)}
/>
</div>

View File

@@ -590,7 +590,7 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
<AIInterpretationButton
analysisData={analysisData}
analysisType="ziwei"
analysisId={recordId?.toString()}
recordId={recordId}
onConfigClick={() => setShowAIConfig(true)}
/>
</div>

View File

@@ -20,7 +20,7 @@ interface AIInterpretationButtonProps {
analysisData?: any; // 分析数据对象(可选)
analysisMarkdown?: string; // 直接传递的MD内容可选
analysisType: 'bazi' | 'ziwei' | 'yijing';
analysisId?: string; // 用于缓存解读结果
recordId?: number; // 分析记录ID用于AI解读
className?: string;
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
@@ -33,7 +33,7 @@ const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
analysisData,
analysisMarkdown,
analysisType,
analysisId,
recordId,
className,
variant = 'default',
size = 'md',
@@ -55,56 +55,14 @@ const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
setIsConfigValid(validateAIConfig(config));
}, []);
// 生成唯一的分析ID包含分析数据的时间戳
const generateAnalysisId = () => {
if (analysisId) {
return analysisId;
}
// 尝试从分析数据中提取时间戳
let timestamp = '';
if (analysisData) {
// 检查多种可能的时间戳字段
const timeFields = [
analysisData.created_at,
analysisData.timestamp,
analysisData.analysis_time,
analysisData.basic_info?.created_at,
analysisData.basic_info?.timestamp,
analysisData.basic_info?.analysis_time
];
for (const field of timeFields) {
if (field) {
timestamp = new Date(field).getTime().toString();
break;
}
}
// 如果没有找到时间戳,使用数据的哈希值作为标识
if (!timestamp) {
const dataString = JSON.stringify(analysisData);
// 使用简单的哈希算法替代btoa避免Unicode字符问题
let hash = 0;
for (let i = 0; i < dataString.length; i++) {
const char = dataString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
timestamp = Math.abs(hash).toString(36).slice(0, 16); // 使用36进制表示
}
}
return `${analysisType}-${timestamp || Date.now()}`;
};
const uniqueAnalysisId = generateAnalysisId();
// 如果没有recordId则无法进行AI解读
const canPerformAI = !!recordId;
// 加载已保存的解读结果
useEffect(() => {
const loadSavedInterpretation = async () => {
if (uniqueAnalysisId) {
const savedInterpretation = await getAIInterpretation(uniqueAnalysisId);
if (recordId) {
const savedInterpretation = await getAIInterpretation(recordId);
if (savedInterpretation) {
setInterpretation(savedInterpretation);
}
@@ -112,7 +70,7 @@ const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
};
loadSavedInterpretation();
}, [uniqueAnalysisId]);
}, [recordId]);
// 处理AI解读请求
const handleAIInterpretation = async () => {
@@ -157,9 +115,9 @@ const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
setStreamingContent(''); // 清空流式内容,使用最终结果
// 保存解读结果
if (uniqueAnalysisId) {
if (recordId) {
try {
await saveAIInterpretation(uniqueAnalysisId, result, analysisType);
await saveAIInterpretation(recordId, result);
} catch (saveError) {
// 保存失败不影响用户体验,静默处理
}
@@ -213,7 +171,7 @@ const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
handleAIInterpretation();
}
}}
disabled={isLoading || (!isConfigValid && !interpretation)}
disabled={isLoading || !canPerformAI || (!isConfigValid && !interpretation)}
className={cn(
'min-h-[40px] min-w-[100px] px-3 sm:px-6 text-xs sm:text-sm flex-shrink-0 whitespace-nowrap',
!isConfigValid && !interpretation && 'opacity-50 cursor-not-allowed'
@@ -263,7 +221,17 @@ const AIInterpretationButton: React.FC<AIInterpretationButtonProps> = ({
</div>
{/* 配置提示 */}
{!isConfigValid && !interpretation && (
{!canPerformAI && (
<div className="flex items-center space-x-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0" />
<div className="text-sm text-red-800">
<p className="font-medium">使AI解读</p>
<p className="text-xs mt-1">IDAI解读结果</p>
</div>
</div>
)}
{canPerformAI && !isConfigValid && !interpretation && (
<div className="flex items-center space-x-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-yellow-600 flex-shrink-0" />
<div className="text-sm text-yellow-800">

View File

@@ -353,18 +353,17 @@ class LocalApiClient {
// AI解读相关方法
aiInterpretation = {
// 获取AI解读状态
get: async (analysisId: number): Promise<ApiResponse<any>> => {
return this.request<any>(`/ai-interpretation/get/${analysisId}`);
// 获取AI解读结果
get: async (readingId: number): Promise<ApiResponse<any>> => {
return this.request(`/ai-interpretation/get/${readingId}`);
},
// 保存AI解读结果
save: async (analysisId: number, content: string, analysisType: string, model?: string, tokensUsed?: number): Promise<ApiResponse<any>> => {
save: async (readingId: number, content: string, model?: string, tokensUsed?: number): Promise<ApiResponse<any>> => {
return this.request<any>('/ai-interpretation/save', {
method: 'POST',
body: JSON.stringify({
analysis_id: analysisId,
analysis_type: analysisType,
reading_id: readingId,
content,
model,
tokens_used: tokensUsed,
@@ -373,16 +372,16 @@ class LocalApiClient {
});
},
// 获取用户的所有AI解读记录
list: async (params?: { page?: number; limit?: number; analysis_type?: string }): Promise<ApiResponse<any[]>> => {
// 获取AI解读列表
list: async (params?: { page?: number; limit?: number; reading_type?: string }): Promise<ApiResponse<any[]>> => {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.append('page', params.page.toString());
if (params?.limit) queryParams.append('limit', params.limit.toString());
if (params?.analysis_type) queryParams.append('analysis_type', params.analysis_type);
if (params?.reading_type) queryParams.append('reading_type', params.reading_type);
const endpoint = `/ai-interpretation/list${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return this.request<any[]>(endpoint);
},
}
};
// 兼容Supabase的functions.invoke方法

View File

@@ -180,24 +180,6 @@ const AnalysisPage: React.FC = () => {
}
}, 100);
// 分析完成后,保存历史记录
try {
const inputData = analysisType === 'yijing' ?
{ question: formData.question, divination_method: 'time' } :
{
name: formData.name,
birth_date: formData.birth_date,
birth_time: formData.birth_time,
birth_place: formData.birth_place,
gender: formData.gender
};
await localApi.analysis.saveHistory(analysisType, analysisData, inputData);
// 历史记录保存成功
} catch (historyError: any) {
// 静默处理历史记录保存错误
}
toast.success('分析完成!');
} catch (error: any) {
toast.error('分析失败:' + (error.message || '未知错误'));

View File

@@ -90,15 +90,11 @@ const HistoryPage: React.FC = () => {
setReadings(processedData);
// 检查每个记录的AI解读状态
// 从后端返回的数据中提取AI解读状态
const aiStatus: {[key: number]: boolean} = {};
for (const reading of processedData) {
try {
const aiResponse = await localApi.aiInterpretation.get(reading.id);
aiStatus[reading.id] = !aiResponse.error && !!aiResponse.data;
} catch {
aiStatus[reading.id] = false;
}
// 使用后端返回的has_ai_interpretation字段
aiStatus[reading.id] = !!(reading as any).has_ai_interpretation;
}
setAiInterpretations(aiStatus);
} catch (error: any) {
@@ -124,8 +120,8 @@ const HistoryPage: React.FC = () => {
throw new Error(response.error.message);
}
setReadings(prev => prev.filter(r => r.id !== readingId));
if (selectedReading?.id === readingId) {
setReadings(prev => prev.filter(r => r.id !== parseInt(readingId)));
if (selectedReading?.id === parseInt(readingId)) {
setSelectedReading(null);
setViewingResult(false);
}
@@ -227,7 +223,7 @@ const HistoryPage: React.FC = () => {
divinationMethod={selectedReading.reading_type === 'yijing' ?
getInputDataValue(selectedReading.input_data, 'divination_method', 'time') : undefined}
preAnalysisData={selectedReading.analysis}
recordId={parseInt(selectedReading.id)}
recordId={selectedReading.id}
/>
</div>
@@ -345,7 +341,7 @@ const HistoryPage: React.FC = () => {
<ChineseButton
variant="ghost"
size="md"
onClick={() => handleDeleteReading(reading.id)}
onClick={() => handleDeleteReading(reading.id.toString())}
className="min-h-[40px] text-red-600 hover:text-red-700 hover:bg-red-50 px-2 sm:px-3 flex-shrink-0"
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4" />

View File

@@ -875,7 +875,7 @@ export const requestAIInterpretation = async (request: AIInterpretationRequest):
};
// 保存AI解读结果到数据库
export const saveAIInterpretation = async (analysisId: string, result: AIInterpretationResult, analysisType: string): Promise<void> => {
export const saveAIInterpretation = async (readingId: number, result: AIInterpretationResult): Promise<void> => {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
@@ -893,8 +893,7 @@ export const saveAIInterpretation = async (analysisId: string, result: AIInterpr
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
analysis_id: parseInt(analysisId),
analysis_type: analysisType,
reading_id: readingId,
content: result.content,
model: result.model,
tokens_used: result.tokensUsed,
@@ -908,12 +907,12 @@ export const saveAIInterpretation = async (analysisId: string, result: AIInterpr
}
// 同时保存到localStorage作为备份
const key = `ai-interpretation-${analysisId}`;
const key = `ai-interpretation-${readingId}`;
localStorage.setItem(key, JSON.stringify(result));
} catch (error) {
// 如果数据库保存失败至少保存到localStorage
try {
const key = `ai-interpretation-${analysisId}`;
const key = `ai-interpretation-${readingId}`;
localStorage.setItem(key, JSON.stringify(result));
} catch (localError) {
// 静默处理存储错误
@@ -922,7 +921,7 @@ export const saveAIInterpretation = async (analysisId: string, result: AIInterpr
};
// 从数据库或本地存储获取AI解读结果
export const getAIInterpretation = async (analysisId: string): Promise<AIInterpretationResult | null> => {
export const getAIInterpretation = async (readingId: number): Promise<AIInterpretationResult | null> => {
try {
const token = localStorage.getItem('auth_token');
if (token) {
@@ -931,7 +930,7 @@ export const getAIInterpretation = async (analysisId: string): Promise<AIInterpr
(import.meta.env.DEV ? 'http://localhost:3001/api' :
(window.location.hostname.includes('koyeb.app') ? `${window.location.origin}/api` : `${window.location.origin}/api`));
const response = await fetch(`${API_BASE_URL}/ai-interpretation/get/${analysisId}`, {
const response = await fetch(`${API_BASE_URL}/ai-interpretation/get/${readingId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
@@ -953,7 +952,7 @@ export const getAIInterpretation = async (analysisId: string): Promise<AIInterpr
}
// 如果数据库获取失败尝试从localStorage获取
const key = `ai-interpretation-${analysisId}`;
const key = `ai-interpretation-${readingId}`;
const saved = localStorage.getItem(key);
if (saved) {
return JSON.parse(saved);
@@ -961,7 +960,7 @@ export const getAIInterpretation = async (analysisId: string): Promise<AIInterpr
} catch (error) {
// 如果数据库获取失败尝试从localStorage获取
try {
const key = `ai-interpretation-${analysisId}`;
const key = `ai-interpretation-${readingId}`;
const saved = localStorage.getItem(key);
if (saved) {
return JSON.parse(saved);

View File

@@ -1,6 +1,6 @@
export interface UserProfile {
id: string;
user_id: string;
id: number;
user_id: number;
username?: string;
full_name: string;
birth_date: string;
@@ -13,8 +13,8 @@ export interface UserProfile {
}
export interface AnalysisRecord {
id: string;
user_id: string;
id: number;
user_id: number;
analysis_type: 'bazi' | 'ziwei' | 'yijing';
name: string;
birth_date: string;
@@ -29,8 +29,8 @@ export interface AnalysisRecord {
}
export interface NumerologyReading {
id: string;
user_id: string;
id: number;
user_id: number;
profile_id?: string;
reading_type: 'bazi' | 'ziwei' | 'yijing' | 'comprehensive';
name: string;
@@ -56,7 +56,7 @@ export interface NumerologyReading {
}
export interface AnalysisRequest {
user_id: string;
user_id: number;
birth_data: {
name: string;
birth_date: string;