mirror of
https://github.com/patdelphi/suanming.git
synced 2026-03-11 02:53:11 +08:00
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:
@@ -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} />;
|
||||
}
|
||||
|
||||
// 如果没有分析结果数据
|
||||
|
||||
@@ -287,7 +287,7 @@ const CompleteBaziAnalysis: React.FC<CompleteBaziAnalysisProps> = ({ birthDate,
|
||||
<AIInterpretationButton
|
||||
analysisData={analysisData}
|
||||
analysisType="bazi"
|
||||
analysisId={recordId?.toString()}
|
||||
recordId={recordId}
|
||||
onConfigClick={() => setShowAIConfig(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -276,7 +276,7 @@ const CompleteYijingAnalysis: React.FC<CompleteYijingAnalysisProps> = ({
|
||||
<AIInterpretationButton
|
||||
analysisData={analysisData}
|
||||
analysisType="yijing"
|
||||
analysisId={recordId?.toString()}
|
||||
recordId={recordId}
|
||||
onConfigClick={() => setShowAIConfig(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -590,7 +590,7 @@ const CompleteZiweiAnalysis: React.FC<CompleteZiweiAnalysisProps> = ({ birthDate
|
||||
<AIInterpretationButton
|
||||
analysisData={analysisData}
|
||||
analysisType="ziwei"
|
||||
analysisId={recordId?.toString()}
|
||||
recordId={recordId}
|
||||
onConfigClick={() => setShowAIConfig(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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">此分析记录没有有效的ID,无法保存AI解读结果</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">
|
||||
|
||||
@@ -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方法
|
||||
|
||||
@@ -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 || '未知错误'));
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user