diff --git a/cleanup-duplicate-records.cjs b/cleanup-duplicate-records.cjs deleted file mode 100644 index 46439e1..0000000 --- a/cleanup-duplicate-records.cjs +++ /dev/null @@ -1,202 +0,0 @@ -const { getDB } = require('./server/database/index.cjs'); - -/** - * 清理重复记录的脚本 - * 保留每组重复记录中最新的一条,删除其他重复记录 - * 重新关联AI解读记录到保留的记录 - */ -function cleanupDuplicateRecords() { - try { - // 初始化数据库连接 - const { dbManager } = require('./server/database/index.cjs'); - dbManager.init(); - const db = getDB(); - - console.log('=== 开始清理重复记录 ===\n'); - - // 开始事务 - db.exec('BEGIN TRANSACTION'); - - try { - // 1. 找出所有重复记录组 - console.log('1. 识别重复记录组...'); - const duplicateGroups = db.prepare(` - SELECT - name, - reading_type, - user_id, - COUNT(*) as count, - GROUP_CONCAT(id ORDER BY created_at DESC) as ids, - MAX(id) as keep_id - FROM numerology_readings - GROUP BY name, reading_type, user_id - HAVING COUNT(*) > 1 - ORDER BY count DESC - `).all(); - - console.log(`找到 ${duplicateGroups.length} 个重复记录组`); - - let totalDuplicates = 0; - let totalDeleted = 0; - let aiRecordsUpdated = 0; - - // 2. 处理每个重复组 - for (const group of duplicateGroups) { - const ids = group.ids.split(',').map(id => parseInt(id)); - const keepId = group.keep_id; // 保留最新的记录 - const deleteIds = ids.filter(id => id !== keepId); - - totalDuplicates += (group.count - 1); - - console.log(`\n处理组: ${group.name} (${group.reading_type})`); - console.log(` 总记录: ${group.count}条`); - console.log(` 保留记录: ID ${keepId}`); - console.log(` 删除记录: ${deleteIds.join(', ')}`); - - // 3. 更新AI解读记录的关联 - for (const deleteId of deleteIds) { - const aiRecords = db.prepare(` - SELECT id FROM ai_interpretations - WHERE analysis_id = ? AND user_id = ? - `).all(deleteId.toString(), group.user_id); - - if (aiRecords.length > 0) { - console.log(` 发现 ${aiRecords.length} 条AI解读记录需要重新关联`); - - // 检查目标记录是否已有AI解读 - const existingAI = db.prepare(` - SELECT id FROM ai_interpretations - WHERE analysis_id = ? AND user_id = ? - `).get(keepId.toString(), group.user_id); - - if (existingAI) { - console.log(` 目标记录已有AI解读,删除重复的AI解读记录`); - // 删除重复的AI解读记录 - const deleteAI = db.prepare(` - DELETE FROM ai_interpretations - WHERE analysis_id = ? AND user_id = ? - `); - deleteAI.run(deleteId.toString(), group.user_id); - } else { - console.log(` 重新关联AI解读记录到保留的记录`); - // 更新AI解读记录的analysis_id - const updateAI = db.prepare(` - UPDATE ai_interpretations - SET analysis_id = ? - WHERE analysis_id = ? AND user_id = ? - `); - const result = updateAI.run(keepId.toString(), deleteId.toString(), group.user_id); - aiRecordsUpdated += result.changes; - } - } - } - - // 4. 删除重复的历史记录 - if (deleteIds.length > 0) { - const deleteStmt = db.prepare(` - DELETE FROM numerology_readings - WHERE id IN (${deleteIds.map(() => '?').join(',')}) - `); - const result = deleteStmt.run(...deleteIds); - totalDeleted += result.changes; - console.log(` 删除了 ${result.changes} 条重复记录`); - } - } - - // 5. 清理无法关联的AI解读记录 - console.log('\n5. 清理无法关联的AI解读记录...'); - const orphanedAI = db.prepare(` - SELECT ai.id, ai.analysis_id, ai.analysis_type - FROM ai_interpretations ai - LEFT JOIN numerology_readings nr ON ai.analysis_id = nr.id AND ai.user_id = nr.user_id - WHERE nr.id IS NULL - `).all(); - - if (orphanedAI.length > 0) { - console.log(`发现 ${orphanedAI.length} 条无法关联的AI解读记录`); - orphanedAI.forEach(record => { - console.log(` AI记录 ${record.id}: analysis_id=${record.analysis_id}, type=${record.analysis_type}`); - }); - - // 删除无法关联的AI解读记录 - const deleteOrphanedAI = db.prepare(` - DELETE FROM ai_interpretations - WHERE id IN (${orphanedAI.map(() => '?').join(',')}) - `); - const result = deleteOrphanedAI.run(...orphanedAI.map(r => r.id)); - console.log(`删除了 ${result.changes} 条无法关联的AI解读记录`); - } else { - console.log('没有发现无法关联的AI解读记录'); - } - - // 提交事务 - db.exec('COMMIT'); - - // 6. 输出清理结果 - console.log('\n=== 清理完成 ==='); - console.log(`处理的重复组: ${duplicateGroups.length}`); - console.log(`总重复记录: ${totalDuplicates}`); - console.log(`删除的记录: ${totalDeleted}`); - console.log(`更新的AI解读: ${aiRecordsUpdated}`); - console.log(`删除的孤立AI解读: ${orphanedAI.length}`); - - // 7. 验证清理结果 - console.log('\n=== 验证清理结果 ==='); - const remainingDuplicates = db.prepare(` - SELECT COUNT(*) as count - FROM ( - SELECT name, reading_type, user_id, COUNT(*) as cnt - FROM numerology_readings - GROUP BY name, reading_type, user_id - HAVING COUNT(*) > 1 - ) - `).get(); - - console.log(`剩余重复记录组: ${remainingDuplicates.count}`); - - const totalRecords = db.prepare(` - SELECT COUNT(*) as count FROM numerology_readings - `).get(); - - console.log(`当前历史记录总数: ${totalRecords.count}`); - - const totalAI = db.prepare(` - SELECT COUNT(*) as count FROM ai_interpretations - `).get(); - - console.log(`当前AI解读记录总数: ${totalAI.count}`); - - const matchedAI = db.prepare(` - SELECT COUNT(*) as count - FROM ai_interpretations ai - JOIN numerology_readings nr ON ai.analysis_id = nr.id AND ai.user_id = nr.user_id - `).get(); - - console.log(`成功关联的AI解读: ${matchedAI.count}`); - console.log(`AI解读关联率: ${((matchedAI.count / totalAI.count) * 100).toFixed(1)}%`); - - } catch (error) { - // 回滚事务 - db.exec('ROLLBACK'); - throw error; - } - - } catch (error) { - console.error('清理过程中发生错误:', error); - throw error; - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - try { - cleanupDuplicateRecords(); - console.log('\n✅ 数据清理成功完成'); - process.exit(0); - } catch (error) { - console.error('\n❌ 数据清理失败:', error); - process.exit(1); - } -} - -module.exports = { cleanupDuplicateRecords }; \ No newline at end of file diff --git a/debug-ai-button-status.cjs b/debug-ai-button-status.cjs deleted file mode 100644 index 3f61dbf..0000000 --- a/debug-ai-button-status.cjs +++ /dev/null @@ -1,180 +0,0 @@ -/** - * 调试AI解读按钮状态的脚本 - * 检查AI配置和recordId传递情况 - */ - -// 模拟前端AI配置检查 -function checkAIConfig() { - console.log('=== AI配置检查 ==='); - - // 模拟默认配置 - const defaultConfig = { - apiKey: 'dee444451bdf4232920a88ef430ce753.Z4SAbECrSnf5JMq7', - apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', - modelName: 'GLM-4.5', - maxTokens: 50000, - temperature: 0.6, - timeout: 120000, - stream: true - }; - - console.log('默认AI配置:'); - console.log(JSON.stringify(defaultConfig, null, 2)); - - // 验证配置 - const isValid = !!(defaultConfig.apiKey && defaultConfig.apiUrl && defaultConfig.modelName); - console.log(`AI配置是否有效: ${isValid}`); - - return isValid; -} - -// 检查数据库中的记录和AI解读状态 -function checkRecordStatus() { - try { - const { getDB } = require('./server/database/index.cjs'); - const { dbManager } = require('./server/database/index.cjs'); - - dbManager.init(); - const db = getDB(); - - console.log('\n=== 数据库记录状态检查 ==='); - - // 检查最近的历史记录 - const recentRecords = db.prepare(` - SELECT - id, - name, - reading_type, - created_at, - datetime(created_at, 'localtime') as local_time - FROM numerology_readings - ORDER BY created_at DESC - LIMIT 10 - `).all(); - - console.log('\n最近的历史记录:'); - recentRecords.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id}, name: ${record.name}, type: ${record.reading_type}, created: ${record.local_time}`); - - // 检查是否有对应的AI解读 - const aiRecord = db.prepare(` - SELECT id, success FROM ai_interpretations - WHERE analysis_id = ? AND user_id = ( - SELECT user_id FROM numerology_readings WHERE id = ? - ) - `).get(record.id.toString(), record.id); - - if (aiRecord) { - console.log(` → 有AI解读记录 (ID: ${aiRecord.id}, success: ${aiRecord.success})`); - } else { - console.log(` → 无AI解读记录`); - } - }); - - // 检查AI解读记录 - const aiRecords = db.prepare(` - SELECT - id, - analysis_id, - analysis_type, - success, - created_at, - datetime(created_at, 'localtime') as local_time - FROM ai_interpretations - ORDER BY created_at DESC - LIMIT 5 - `).all(); - - console.log('\n最近的AI解读记录:'); - aiRecords.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id}, analysis_id: ${record.analysis_id}, type: ${record.analysis_type}, success: ${record.success}, created: ${record.local_time}`); - }); - - return { recentRecords, aiRecords }; - - } catch (error) { - console.error('检查数据库记录时发生错误:', error); - return null; - } -} - -// 模拟AI解读按钮状态检查 -function simulateButtonStatus(recordId, hasAIConfig) { - console.log('\n=== AI解读按钮状态模拟 ==='); - - const hasValidId = !!recordId; - const isConfigValid = hasAIConfig; - - console.log(`recordId: ${recordId}`); - console.log(`hasValidId: ${hasValidId}`); - console.log(`isConfigValid: ${isConfigValid}`); - - // 模拟按钮禁用逻辑 - const isDisabled = !isConfigValid || !hasValidId; - - console.log(`按钮是否禁用: ${isDisabled}`); - - if (isDisabled) { - console.log('禁用原因:'); - if (!isConfigValid) { - console.log(' - AI配置无效'); - } - if (!hasValidId) { - console.log(' - 缺少有效的recordId'); - } - } else { - console.log('按钮应该可以点击'); - } - - return !isDisabled; -} - -// 主函数 -function debugAIButtonStatus() { - console.log('=== AI解读按钮状态调试 ===\n'); - - // 1. 检查AI配置 - const isAIConfigValid = checkAIConfig(); - - // 2. 检查数据库记录 - const dbStatus = checkRecordStatus(); - - if (dbStatus && dbStatus.recentRecords.length > 0) { - // 3. 模拟最新记录的按钮状态 - const latestRecord = dbStatus.recentRecords[0]; - console.log(`\n=== 模拟最新记录 (ID: ${latestRecord.id}) 的按钮状态 ===`); - - const canClick = simulateButtonStatus(latestRecord.id, isAIConfigValid); - - console.log('\n=== 总结 ==='); - console.log(`最新记录: ${latestRecord.name} (${latestRecord.reading_type})`); - console.log(`记录ID: ${latestRecord.id}`); - console.log(`AI配置有效: ${isAIConfigValid}`); - console.log(`AI解读按钮可点击: ${canClick}`); - - if (!canClick) { - console.log('\n🔧 解决建议:'); - if (!isAIConfigValid) { - console.log('1. 检查AI配置是否正确设置'); - console.log('2. 确认API密钥、URL和模型名称都已配置'); - } - if (!latestRecord.id) { - console.log('1. 检查分析结果保存逻辑'); - console.log('2. 确认recordId正确传递给AI解读按钮'); - } - } - } else { - console.log('\n❌ 没有找到历史记录,请先进行分析'); - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - try { - debugAIButtonStatus(); - } catch (error) { - console.error('调试过程中发生错误:', error); - } -} - -module.exports = { debugAIButtonStatus }; \ No newline at end of file diff --git a/debug-ai-interpretation-matching.cjs b/debug-ai-interpretation-matching.cjs deleted file mode 100644 index 03ef28d..0000000 --- a/debug-ai-interpretation-matching.cjs +++ /dev/null @@ -1,191 +0,0 @@ -const { getDB } = require('./server/database/index.cjs'); - -/** - * 调试AI解读记录匹配问题 - * 检查历史记录与AI解读记录的ID匹配情况 - */ -function debugAIInterpretationMatching() { - try { - // 初始化数据库连接 - const { dbManager } = require('./server/database/index.cjs'); - dbManager.init(); - const db = getDB(); - - console.log('=== AI解读记录匹配调试 ===\n'); - - // 1. 获取最近的历史记录 - console.log('1. 最近的历史记录:'); - const recentRecords = db.prepare(` - SELECT - id, - name, - reading_type, - created_at, - datetime(created_at, 'localtime') as local_time - FROM numerology_readings - ORDER BY created_at DESC - LIMIT 10 - `).all(); - - recentRecords.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id} (${typeof record.id}), name: ${record.name}, type: ${record.reading_type}, created: ${record.local_time}`); - }); - - // 2. 获取所有AI解读记录 - console.log('\n2. 所有AI解读记录:'); - const aiRecords = db.prepare(` - SELECT - id, - analysis_id, - analysis_type, - success, - created_at, - datetime(created_at, 'localtime') as local_time - FROM ai_interpretations - ORDER BY created_at DESC - `).all(); - - aiRecords.forEach((record, index) => { - console.log(` ${index + 1}. AI_ID: ${record.id}, analysis_id: ${record.analysis_id} (${typeof record.analysis_id}), type: ${record.analysis_type}, success: ${record.success}, created: ${record.local_time}`); - }); - - // 3. 检查每个历史记录是否有对应的AI解读 - console.log('\n3. 历史记录与AI解读匹配检查:'); - recentRecords.forEach((record, index) => { - const recordIdStr = record.id.toString(); - const recordIdNum = parseInt(record.id); - - // 尝试字符串匹配 - const aiByString = db.prepare(` - SELECT id, analysis_id, success FROM ai_interpretations - WHERE analysis_id = ? - `).get(recordIdStr); - - // 尝试数字匹配 - const aiByNumber = db.prepare(` - SELECT id, analysis_id, success FROM ai_interpretations - WHERE analysis_id = ? - `).get(recordIdNum); - - console.log(` ${index + 1}. 历史记录 ${record.id}:`); - console.log(` - 字符串匹配 '${recordIdStr}': ${aiByString ? `找到 AI_ID=${aiByString.id}` : '未找到'}`); - console.log(` - 数字匹配 ${recordIdNum}: ${aiByNumber ? `找到 AI_ID=${aiByNumber.id}` : '未找到'}`); - - if (aiByString || aiByNumber) { - const ai = aiByString || aiByNumber; - console.log(` → 有AI解读: analysis_id=${ai.analysis_id}, success=${ai.success}`); - } else { - console.log(` → 无AI解读记录`); - } - }); - - // 4. 检查AI解读记录是否有对应的历史记录 - console.log('\n4. AI解读记录与历史记录匹配检查:'); - aiRecords.forEach((aiRecord, index) => { - const analysisIdStr = aiRecord.analysis_id.toString(); - const analysisIdNum = parseInt(aiRecord.analysis_id); - - // 尝试字符串匹配 - const historyByString = db.prepare(` - SELECT id, name, reading_type FROM numerology_readings - WHERE id = ? - `).get(analysisIdStr); - - // 尝试数字匹配 - const historyByNumber = db.prepare(` - SELECT id, name, reading_type FROM numerology_readings - WHERE id = ? - `).get(analysisIdNum); - - console.log(` ${index + 1}. AI解读 analysis_id=${aiRecord.analysis_id}:`); - console.log(` - 字符串匹配 '${analysisIdStr}': ${historyByString ? `找到 ${historyByString.name} (${historyByString.reading_type})` : '未找到'}`); - console.log(` - 数字匹配 ${analysisIdNum}: ${historyByNumber ? `找到 ${historyByNumber.name} (${historyByNumber.reading_type})` : '未找到'}`); - - if (!historyByString && !historyByNumber) { - console.log(` → 孤立的AI解读记录,无对应历史记录`); - } - }); - - // 5. 模拟前端API调用 - console.log('\n5. 模拟前端API调用:'); - const testIds = recentRecords.slice(0, 3).map(r => r.id); - - testIds.forEach(id => { - const idStr = id.toString(); - console.log(`\n 测试ID: ${id} (string: '${idStr}')`); - - // 模拟API调用逻辑 - const aiRecord = db.prepare(` - SELECT - ai.id, - ai.analysis_id, - ai.success, - ai.content, - ai.created_at, - ai.model, - ai.tokens_used, - ai.error_message, - nr.name, - nr.reading_type - FROM ai_interpretations ai - LEFT JOIN numerology_readings nr ON ai.analysis_id = nr.id - WHERE ai.analysis_id = ? AND ai.user_id = ( - SELECT user_id FROM numerology_readings WHERE id = ? - ) - `).get(idStr, id); - - if (aiRecord) { - console.log(` ✅ API会返回: success=${aiRecord.success}, content长度=${aiRecord.content?.length || 0}`); - console.log(` 关联记录: ${aiRecord.name} (${aiRecord.reading_type})`); - } else { - console.log(` ❌ API会返回: 404 Not Found`); - } - }); - - // 6. 总结 - console.log('\n=== 总结 ==='); - const totalHistory = recentRecords.length; - const totalAI = aiRecords.length; - const matchedCount = recentRecords.filter(record => { - const recordIdStr = record.id.toString(); - const recordIdNum = parseInt(record.id); - const aiByString = db.prepare(`SELECT id FROM ai_interpretations WHERE analysis_id = ?`).get(recordIdStr); - const aiByNumber = db.prepare(`SELECT id FROM ai_interpretations WHERE analysis_id = ?`).get(recordIdNum); - return aiByString || aiByNumber; - }).length; - - console.log(`历史记录总数: ${totalHistory}`); - console.log(`AI解读记录总数: ${totalAI}`); - console.log(`匹配成功数: ${matchedCount}`); - console.log(`匹配率: ${((matchedCount / totalHistory) * 100).toFixed(1)}%`); - - if (matchedCount === 0) { - console.log('\n⚠️ 没有任何匹配的记录,可能的原因:'); - console.log('1. AI解读记录的analysis_id与历史记录的id不匹配'); - console.log('2. 数据类型不一致(字符串 vs 数字)'); - console.log('3. AI解读记录被意外删除或损坏'); - } else if (matchedCount < totalHistory) { - console.log(`\n⚠️ 部分记录不匹配,${totalHistory - matchedCount}条历史记录没有AI解读`); - } else { - console.log('\n✅ 所有记录都有对应的AI解读'); - } - - } catch (error) { - console.error('调试过程中发生错误:', error); - throw error; - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - try { - debugAIInterpretationMatching(); - console.log('\n✅ 调试完成'); - process.exit(0); - } catch (error) { - console.error('\n❌ 调试失败:', error); - process.exit(1); - } -} - -module.exports = { debugAIInterpretationMatching }; \ No newline at end of file diff --git a/debug-ai-interpretations.cjs b/debug-ai-interpretations.cjs deleted file mode 100644 index e0cc3cb..0000000 --- a/debug-ai-interpretations.cjs +++ /dev/null @@ -1,158 +0,0 @@ -const { getDB } = require('./server/database/index.cjs'); - -/** - * 调试AI解读记录与历史记录匹配问题的脚本 - */ -function debugAIInterpretations() { - try { - // 初始化数据库连接 - const { dbManager } = require('./server/database/index.cjs'); - dbManager.init(); - const db = getDB(); - - console.log('=== AI解读记录与历史记录匹配分析 ===\n'); - - // 1. 查看所有AI解读记录 - console.log('1. 所有AI解读记录:'); - const aiInterpretations = db.prepare(` - SELECT - id, - analysis_id, - analysis_type, - created_at, - success - FROM ai_interpretations - ORDER BY created_at DESC - `).all(); - - console.log(`总共有 ${aiInterpretations.length} 条AI解读记录`); - aiInterpretations.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id}, analysis_id: ${record.analysis_id}, type: ${record.analysis_type}, created: ${record.created_at}, success: ${record.success}`); - }); - - console.log('\n2. 所有历史记录:'); - const historyRecords = db.prepare(` - SELECT - id, - name, - reading_type, - created_at - FROM numerology_readings - ORDER BY created_at DESC - `).all(); - - console.log(`总共有 ${historyRecords.length} 条历史记录`); - historyRecords.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id}, name: ${record.name}, type: ${record.reading_type}, created: ${record.created_at}`); - }); - - console.log('\n3. 匹配分析:'); - - // 3.1 数字ID匹配分析 - console.log('\n3.1 数字ID匹配分析:'); - const numericMatches = []; - const numericMismatches = []; - - aiInterpretations.forEach(ai => { - const isNumeric = !isNaN(parseInt(ai.analysis_id)) && isFinite(ai.analysis_id); - if (isNumeric) { - const historyRecord = historyRecords.find(h => h.id.toString() === ai.analysis_id.toString()); - if (historyRecord) { - numericMatches.push({ ai, history: historyRecord }); - } else { - numericMismatches.push(ai); - } - } - }); - - console.log(`数字ID匹配成功: ${numericMatches.length} 条`); - numericMatches.forEach((match, index) => { - console.log(` ${index + 1}. AI记录ID: ${match.ai.id}, analysis_id: ${match.ai.analysis_id} → 历史记录: ${match.history.name} (${match.history.reading_type})`); - }); - - console.log(`\n数字ID匹配失败: ${numericMismatches.length} 条`); - numericMismatches.forEach((mismatch, index) => { - console.log(` ${index + 1}. AI记录ID: ${mismatch.id}, analysis_id: ${mismatch.analysis_id} (找不到对应的历史记录)`); - }); - - // 3.2 字符串ID分析 - console.log('\n3.2 字符串ID分析:'); - const stringIds = aiInterpretations.filter(ai => { - const isNumeric = !isNaN(parseInt(ai.analysis_id)) && isFinite(ai.analysis_id); - return !isNumeric; - }); - - console.log(`字符串ID记录: ${stringIds.length} 条`); - stringIds.forEach((record, index) => { - console.log(` ${index + 1}. AI记录ID: ${record.id}, analysis_id: ${record.analysis_id}, type: ${record.analysis_type}`); - }); - - // 3.3 没有AI解读的历史记录 - console.log('\n3.3 没有AI解读的历史记录:'); - const recordsWithoutAI = []; - - historyRecords.forEach(history => { - const hasAI = aiInterpretations.some(ai => { - const isNumeric = !isNaN(parseInt(ai.analysis_id)) && isFinite(ai.analysis_id); - if (isNumeric) { - return ai.analysis_id.toString() === history.id.toString(); - } - return false; // 字符串ID暂时不匹配 - }); - - if (!hasAI) { - recordsWithoutAI.push(history); - } - }); - - console.log(`没有AI解读的历史记录: ${recordsWithoutAI.length} 条`); - recordsWithoutAI.forEach((record, index) => { - console.log(` ${index + 1}. 历史记录ID: ${record.id}, name: ${record.name}, type: ${record.reading_type}, created: ${record.created_at}`); - }); - - // 4. 问题总结 - console.log('\n=== 问题总结 ==='); - console.log(`1. 总AI解读记录: ${aiInterpretations.length}`); - console.log(`2. 总历史记录: ${historyRecords.length}`); - console.log(`3. 数字ID匹配成功: ${numericMatches.length}`); - console.log(`4. 数字ID匹配失败: ${numericMismatches.length}`); - console.log(`5. 字符串ID记录: ${stringIds.length}`); - console.log(`6. 没有AI解读的历史记录: ${recordsWithoutAI.length}`); - - // 5. 可能的问题原因 - console.log('\n=== 可能的问题原因 ==='); - if (numericMismatches.length > 0) { - console.log('1. 数字ID不匹配问题:'); - console.log(' - AI解读记录的analysis_id指向不存在的历史记录'); - console.log(' - 可能是历史记录被删除,但AI解读记录仍然存在'); - } - - if (stringIds.length > 0) { - console.log('2. 字符串ID问题:'); - console.log(' - 这些AI解读使用了生成的字符串ID(如"yijing-1234567890")'); - console.log(' - 前端查询时无法找到对应的历史记录'); - console.log(' - 需要改进匹配逻辑或数据关联方式'); - } - - if (recordsWithoutAI.length > 0) { - console.log('3. 历史记录缺少AI解读:'); - console.log(' - 这些记录可能是在AI解读功能实现之前创建的'); - console.log(' - 或者AI解读生成失败但历史记录保存成功'); - } - - console.log('\n=== 建议的解决方案 ==='); - console.log('1. 对于数字ID不匹配: 清理无效的AI解读记录'); - console.log('2. 对于字符串ID: 改进前端匹配逻辑,支持字符串ID查询'); - console.log('3. 对于缺少AI解读的历史记录: 可以提供重新生成AI解读的功能'); - - } catch (error) { - console.error('调试过程中发生错误:', error); - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - debugAIInterpretations(); -} - -module.exports = { debugAIInterpretations }; \ No newline at end of file diff --git a/debug-duplicate-records.cjs b/debug-duplicate-records.cjs deleted file mode 100644 index 3f30dfe..0000000 --- a/debug-duplicate-records.cjs +++ /dev/null @@ -1,194 +0,0 @@ -const { getDB } = require('./server/database/index.cjs'); - -/** - * 调试重复记录问题的脚本 - * 检查是否存在AI解读后创建新历史记录的情况 - */ -function debugDuplicateRecords() { - try { - // 初始化数据库连接 - const { dbManager } = require('./server/database/index.cjs'); - dbManager.init(); - const db = getDB(); - - console.log('=== 调试重复记录问题 ===\n'); - - // 1. 检查最近的历史记录 - console.log('1. 最近的历史记录 (按创建时间排序):'); - const recentRecords = db.prepare(` - SELECT - id, - name, - reading_type, - created_at, - datetime(created_at, 'localtime') as local_time - FROM numerology_readings - ORDER BY created_at DESC - LIMIT 20 - `).all(); - - recentRecords.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id}, name: ${record.name}, type: ${record.reading_type}, created: ${record.local_time}`); - }); - - // 2. 检查AI解读记录 - console.log('\n2. AI解读记录:'); - const aiRecords = db.prepare(` - SELECT - id, - analysis_id, - analysis_type, - created_at, - datetime(created_at, 'localtime') as local_time - FROM ai_interpretations - ORDER BY created_at DESC - `).all(); - - aiRecords.forEach((record, index) => { - console.log(` ${index + 1}. ID: ${record.id}, analysis_id: ${record.analysis_id}, type: ${record.analysis_type}, created: ${record.local_time}`); - }); - - // 3. 分析可能的重复模式 - console.log('\n3. 分析重复模式:'); - - // 按名称和类型分组,查找可能的重复记录 - const duplicateGroups = db.prepare(` - SELECT - name, - reading_type, - COUNT(*) as count, - GROUP_CONCAT(id) as ids, - GROUP_CONCAT(datetime(created_at, 'localtime')) as times - FROM numerology_readings - GROUP BY name, reading_type - HAVING COUNT(*) > 1 - ORDER BY count DESC, MAX(created_at) DESC - LIMIT 10 - `).all(); - - if (duplicateGroups.length > 0) { - console.log(' 发现可能的重复记录组:'); - duplicateGroups.forEach((group, index) => { - console.log(` ${index + 1}. ${group.name} (${group.reading_type}): ${group.count}条记录`); - console.log(` IDs: ${group.ids}`); - console.log(` 创建时间: ${group.times}`); - }); - } else { - console.log(' 未发现明显的重复记录组'); - } - - // 4. 检查时间相近的记录 - console.log('\n4. 检查时间相近的记录 (5分钟内):'); - const closeTimeRecords = db.prepare(` - SELECT - r1.id as id1, - r1.name as name1, - r1.reading_type as type1, - r1.created_at as time1, - r2.id as id2, - r2.name as name2, - r2.reading_type as type2, - r2.created_at as time2, - ABS(strftime('%s', r1.created_at) - strftime('%s', r2.created_at)) as time_diff_seconds - FROM numerology_readings r1 - JOIN numerology_readings r2 ON r1.id < r2.id - WHERE r1.name = r2.name - AND r1.reading_type = r2.reading_type - AND ABS(strftime('%s', r1.created_at) - strftime('%s', r2.created_at)) <= 300 - ORDER BY time_diff_seconds ASC - LIMIT 10 - `).all(); - - if (closeTimeRecords.length > 0) { - console.log(' 发现时间相近的相似记录:'); - closeTimeRecords.forEach((pair, index) => { - console.log(` ${index + 1}. ${pair.name1} (${pair.type1}):`); - console.log(` 记录1: ID ${pair.id1}, 时间: ${pair.time1}`); - console.log(` 记录2: ID ${pair.id2}, 时间: ${pair.time2}`); - console.log(` 时间差: ${pair.time_diff_seconds}秒`); - }); - } else { - console.log(' 未发现时间相近的相似记录'); - } - - // 5. 检查AI解读与历史记录的关联情况 - console.log('\n5. AI解读与历史记录关联分析:'); - - // 数字ID关联 - const numericMatches = db.prepare(` - SELECT - ai.id as ai_id, - ai.analysis_id, - ai.analysis_type, - nr.id as record_id, - nr.name, - nr.reading_type, - datetime(ai.created_at, 'localtime') as ai_time, - datetime(nr.created_at, 'localtime') as record_time - FROM ai_interpretations ai - JOIN numerology_readings nr ON ai.analysis_id = nr.id AND ai.user_id = nr.user_id - ORDER BY ai.created_at DESC - `).all(); - - console.log(` 数字ID成功关联: ${numericMatches.length}条`); - numericMatches.forEach((match, index) => { - console.log(` ${index + 1}. AI记录${match.ai_id} → 历史记录${match.record_id} (${match.name}, ${match.reading_type})`); - console.log(` AI创建: ${match.ai_time}, 记录创建: ${match.record_time}`); - }); - - // 字符串ID(无法关联) - const stringIdRecords = db.prepare(` - SELECT - ai.id as ai_id, - ai.analysis_id, - ai.analysis_type, - datetime(ai.created_at, 'localtime') as ai_time - FROM ai_interpretations ai - LEFT JOIN numerology_readings nr ON ai.analysis_id = nr.id AND ai.user_id = nr.user_id - WHERE nr.id IS NULL - ORDER BY ai.created_at DESC - `).all(); - - console.log(`\n 字符串ID无法关联: ${stringIdRecords.length}条`); - stringIdRecords.forEach((record, index) => { - console.log(` ${index + 1}. AI记录${record.ai_id}, analysis_id: ${record.analysis_id}, type: ${record.analysis_type}`); - console.log(` 创建时间: ${record.ai_time}`); - }); - - // 6. 总结分析 - console.log('\n=== 问题分析总结 ==='); - - const totalRecords = recentRecords.length; - const totalAI = aiRecords.length; - const duplicateCount = duplicateGroups.reduce((sum, group) => sum + (group.count - 1), 0); - - console.log(`历史记录总数: ${totalRecords}`); - console.log(`AI解读记录总数: ${totalAI}`); - console.log(`可能的重复记录: ${duplicateCount}条`); - console.log(`成功关联的AI解读: ${numericMatches.length}条`); - console.log(`无法关联的AI解读: ${stringIdRecords.length}条`); - - if (stringIdRecords.length > 0) { - console.log('\n⚠️ 发现问题:'); - console.log(' - 存在无法关联到历史记录的AI解读'); - console.log(' - 这些AI解读使用了字符串ID,不对应任何历史记录'); - console.log(' - 可能的原因:AI解读时没有正确的recordId'); - } - - if (duplicateGroups.length > 0) { - console.log('\n⚠️ 发现重复记录:'); - console.log(' - 存在相同名称和类型的多条历史记录'); - console.log(' - 可能的原因:用户多次提交相同的分析请求'); - } - - } catch (error) { - console.error('调试过程中发生错误:', error); - } -} - -// 如果直接运行此脚本 -if (require.main === module) { - debugDuplicateRecords(); -} - -module.exports = { debugDuplicateRecords }; \ No newline at end of file diff --git a/debug-records.cjs b/debug-records.cjs deleted file mode 100644 index 09cc842..0000000 --- a/debug-records.cjs +++ /dev/null @@ -1,33 +0,0 @@ -const { getDB } = require('./server/database/index.cjs'); -const { dbManager } = require('./server/database/index.cjs'); - -try { - dbManager.init(); - const db = getDB(); - - console.log('=== 最近的分析记录 ==='); - const recent = db.prepare('SELECT id, name, reading_type, created_at FROM numerology_readings ORDER BY created_at DESC LIMIT 5').all(); - console.log(`总共 ${recent.length} 条分析记录`); - recent.forEach((r, i) => { - console.log(`${i+1}. ID: ${r.id}, 名称: ${r.name}, 类型: ${r.reading_type}, 创建时间: ${r.created_at}`); - }); - - console.log('\n=== AI解读记录 ==='); - const ai = db.prepare('SELECT id, reading_id, created_at FROM ai_interpretations ORDER BY created_at DESC LIMIT 5').all(); - console.log(`总共 ${ai.length} 条AI解读记录`); - ai.forEach((r, i) => { - console.log(`${i+1}. AI_ID: ${r.id}, reading_id: ${r.reading_id}, 创建时间: ${r.created_at}`); - }); - - console.log('\n=== 检查最新记录的AI解读状态 ==='); - if (recent.length > 0) { - const latestRecord = recent[0]; - const hasAI = db.prepare('SELECT COUNT(*) as count FROM ai_interpretations WHERE reading_id = ?').get(latestRecord.id); - console.log(`最新记录 ID: ${latestRecord.id} (${latestRecord.reading_type}) 是否有AI解读: ${hasAI.count > 0 ? '是' : '否'}`); - } - -} catch (error) { - console.error('调试失败:', error); -} finally { - process.exit(0); -} \ No newline at end of file diff --git a/server/database/index.cjs b/server/database/index.cjs index 6b112d5..9c51aba 100644 --- a/server/database/index.cjs +++ b/server/database/index.cjs @@ -80,8 +80,12 @@ class DatabaseManager { } } - // 迁移ai_interpretations表结构 + /** + * 迁移ai_interpretations表结构 + * 将旧的analysis_id字段迁移为reading_id字段,建立正确的外键关系 + */ migrateAiInterpretationsTable() { + let transaction; try { // 检查ai_interpretations表是否存在且使用旧的analysis_id字段 const tableInfo = this.db.prepare(` @@ -89,60 +93,100 @@ class DatabaseManager { WHERE type='table' AND name='ai_interpretations' `).get(); - if (tableInfo) { - // 检查是否有analysis_id字段(旧结构) - const columnInfo = this.db.prepare(` - PRAGMA table_info(ai_interpretations) - `).all(); - - const hasAnalysisId = columnInfo.some(col => col.name === 'analysis_id'); - const hasReadingId = columnInfo.some(col => col.name === 'reading_id'); - - if (hasAnalysisId && !hasReadingId) { - console.log('检测到旧的ai_interpretations表结构,开始迁移...'); - - // 创建新表结构 - this.db.exec(` - CREATE TABLE ai_interpretations_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - reading_id INTEGER NOT NULL, - content TEXT NOT NULL, - model TEXT, - tokens_used INTEGER, - success BOOLEAN DEFAULT 1, - error_message TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (reading_id) REFERENCES numerology_readings(id) ON DELETE CASCADE, - UNIQUE(reading_id) - ) - `); - - // 迁移数据(只迁移数字ID的记录) - this.db.exec(` - INSERT INTO ai_interpretations_new - (user_id, reading_id, content, model, tokens_used, success, error_message, created_at, updated_at) - SELECT user_id, CAST(analysis_id AS INTEGER), content, model, tokens_used, success, error_message, created_at, updated_at - FROM ai_interpretations - WHERE analysis_id GLOB '[0-9]*' - `); - - // 删除旧表,重命名新表 - this.db.exec('DROP TABLE ai_interpretations'); - this.db.exec('ALTER TABLE ai_interpretations_new RENAME TO ai_interpretations'); - - // 重新创建索引 - this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_user_id ON ai_interpretations(user_id)'); - this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_reading_id ON ai_interpretations(reading_id)'); - this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_created_at ON ai_interpretations(created_at DESC)'); - - console.log('ai_interpretations表迁移完成'); - } + if (!tableInfo) { + console.log('ai_interpretations表不存在,跳过迁移'); + return; } + + // 检查是否有analysis_id字段(旧结构) + const columnInfo = this.db.prepare(` + PRAGMA table_info(ai_interpretations) + `).all(); + + const hasAnalysisId = columnInfo.some(col => col.name === 'analysis_id'); + const hasReadingId = columnInfo.some(col => col.name === 'reading_id'); + + if (!hasAnalysisId || hasReadingId) { + console.log('ai_interpretations表结构已是最新,跳过迁移'); + return; + } + + console.log('检测到旧的ai_interpretations表结构,开始迁移...'); + + // 开始事务 + transaction = this.db.transaction(() => { + // 创建新表结构 + this.db.exec(` + CREATE TABLE ai_interpretations_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + reading_id INTEGER NOT NULL, + content TEXT NOT NULL, + model TEXT, + tokens_used INTEGER, + success BOOLEAN DEFAULT 1, + error_message TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (reading_id) REFERENCES numerology_readings(id) ON DELETE CASCADE, + UNIQUE(reading_id) + ) + `); + + // 检查有多少条记录需要迁移 + const countStmt = this.db.prepare(` + SELECT COUNT(*) as count FROM ai_interpretations + WHERE analysis_id GLOB '[0-9]*' + `); + const { count } = countStmt.get(); + console.log(`准备迁移 ${count} 条有效记录`); + + // 迁移数据(只迁移数字ID的记录) + const migrateStmt = this.db.prepare(` + INSERT INTO ai_interpretations_new + (user_id, reading_id, content, model, tokens_used, success, error_message, created_at, updated_at) + SELECT user_id, CAST(analysis_id AS INTEGER), content, model, tokens_used, success, error_message, created_at, updated_at + FROM ai_interpretations + WHERE analysis_id GLOB '[0-9]*' + `); + const migrateResult = migrateStmt.run(); + console.log(`成功迁移 ${migrateResult.changes} 条记录`); + + // 删除旧表,重命名新表 + this.db.exec('DROP TABLE ai_interpretations'); + this.db.exec('ALTER TABLE ai_interpretations_new RENAME TO ai_interpretations'); + + // 重新创建索引 + this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_user_id ON ai_interpretations(user_id)'); + this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_reading_id ON ai_interpretations(reading_id)'); + this.db.exec('CREATE INDEX IF NOT EXISTS idx_ai_interpretations_created_at ON ai_interpretations(created_at DESC)'); + }); + + // 执行事务 + transaction(); + console.log('ai_interpretations表迁移完成'); + } catch (error) { console.error('ai_interpretations表迁移失败:', error); + console.error('错误详情:', error.message); + + // 如果事务失败,尝试回滚(SQLite会自动回滚失败的事务) + try { + // 检查是否存在临时表,如果存在则清理 + const tempTableCheck = this.db.prepare(` + SELECT name FROM sqlite_master + WHERE type='table' AND name='ai_interpretations_new' + `).get(); + + if (tempTableCheck) { + this.db.exec('DROP TABLE ai_interpretations_new'); + console.log('已清理临时表'); + } + } catch (cleanupError) { + console.error('清理临时表失败:', cleanupError); + } + // 迁移失败不应该阻止应用启动,只记录错误 } } diff --git a/server/routes/analysis.cjs b/server/routes/analysis.cjs index 839fd89..8bcc770 100644 --- a/server/routes/analysis.cjs +++ b/server/routes/analysis.cjs @@ -21,13 +21,26 @@ const AIEnhancedAnalysis = require('../services/common/AIEnhancedAnalysis.cjs'); // 初始化AI增强分析服务 const aiEnhancedAnalysis = new AIEnhancedAnalysis(); -// 八字分析接口 -router.post('/bazi', authenticate, asyncHandler(async (req, res) => { - const { birth_data } = req.body; +/** + * 通用输入验证函数 + * @param {Object} birth_data - 出生数据 + * @throws {AppError} 验证失败时抛出错误 + */ +function validateBirthData(birth_data) { + if (!birth_data || typeof birth_data !== 'object') { + throw new AppError('缺少必要参数:出生数据', 400, 'MISSING_BIRTH_DATA'); + } - // 输入验证 - if (!birth_data || !birth_data.name || !birth_data.birth_date) { - throw new AppError('缺少必要参数:姓名和出生日期', 400, 'MISSING_BIRTH_DATA'); + if (!birth_data.name || typeof birth_data.name !== 'string' || birth_data.name.trim().length === 0) { + throw new AppError('缺少必要参数:姓名不能为空', 400, 'MISSING_NAME'); + } + + if (birth_data.name.length > 50) { + throw new AppError('姓名长度不能超过50个字符', 400, 'NAME_TOO_LONG'); + } + + if (!birth_data.birth_date || typeof birth_data.birth_date !== 'string') { + throw new AppError('缺少必要参数:出生日期', 400, 'MISSING_BIRTH_DATE'); } // 验证出生日期格式 @@ -36,55 +49,127 @@ router.post('/bazi', authenticate, asyncHandler(async (req, res) => { throw new AppError('出生日期格式应为 YYYY-MM-DD', 400, 'INVALID_DATE_FORMAT'); } + // 验证日期有效性 + const birthDate = new Date(birth_data.birth_date); + if (isNaN(birthDate.getTime())) { + throw new AppError('出生日期无效', 400, 'INVALID_DATE'); + } + + // 验证日期范围 + const currentDate = new Date(); + const minDate = new Date('1900-01-01'); + if (birthDate < minDate || birthDate > currentDate) { + throw new AppError('出生日期必须在1900年至今之间', 400, 'DATE_OUT_OF_RANGE'); + } + // 验证出生时间格式(如果提供) if (birth_data.birth_time) { + if (typeof birth_data.birth_time !== 'string') { + throw new AppError('出生时间格式无效', 400, 'INVALID_TIME_TYPE'); + } + const timeRegex = /^\d{2}:\d{2}$/; if (!timeRegex.test(birth_data.birth_time)) { throw new AppError('出生时间格式应为 HH:MM', 400, 'INVALID_TIME_FORMAT'); } + + const [hours, minutes] = birth_data.birth_time.split(':').map(Number); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + throw new AppError('出生时间无效', 400, 'INVALID_TIME'); + } } + // 验证性别(如果提供) + if (birth_data.gender) { + const validGenders = ['male', 'female', '男', '女', '男性', '女性']; + if (!validGenders.includes(birth_data.gender)) { + throw new AppError('性别必须是 male/female 或 男/女', 400, 'INVALID_GENDER'); + } + } + + // 验证出生地点长度(如果提供) + if (birth_data.birth_place && birth_data.birth_place.length > 100) { + throw new AppError('出生地点长度不能超过100个字符', 400, 'BIRTH_PLACE_TOO_LONG'); + } +} + +/** + * 通用分析响应处理函数 + * @param {Object} analysisResult - 分析结果 + * @param {Object} res - Express响应对象 + */ +function sendAnalysisResponse(analysisResult, res) { + res.json({ + data: { + analysis: analysisResult + } + }); +} + +/** + * 八字分析接口 + * 执行八字命理分析,不存储历史记录 + */ +router.post('/bazi', authenticate, asyncHandler(async (req, res) => { + const { birth_data } = req.body; + + // 使用通用验证函数 + validateBirthData(birth_data); + try { // 执行八字分析(纯分析,不存储历史记录) const analysisResult = await baziAnalyzer.performFullBaziAnalysis(birth_data); - // 只返回分析结果,不存储历史记录 - res.json({ - data: { - analysis: analysisResult - } - }); + // 使用通用响应函数 + sendAnalysisResponse(analysisResult, res); } catch (error) { console.error('八字分析错误:', error); - throw new AppError('八字分析过程中发生错误', 500, 'BAZI_ANALYSIS_ERROR'); + throw new AppError(`八字分析过程中发生错误: ${error.message}`, 500, 'BAZI_ANALYSIS_ERROR'); } })); -// 易经分析接口 +/** + * 易经分析接口 + * 执行易经占卜分析,不存储历史记录 + */ router.post('/yijing', authenticate, asyncHandler(async (req, res) => { const { question, user_id, divination_method, user_timezone, local_time } = req.body; // 输入验证 - if (!question) { - throw new AppError('缺少必要参数:占卜问题', 400, 'MISSING_QUESTION'); + if (!question || typeof question !== 'string' || question.trim().length === 0) { + throw new AppError('缺少必要参数:占卜问题不能为空', 400, 'MISSING_QUESTION'); + } + + if (question.length > 200) { + throw new AppError('占卜问题长度不能超过200个字符', 400, 'QUESTION_TOO_LONG'); + } + + // 验证起卦方法 + if (divination_method) { + const validMethods = ['time', 'plum_blossom', 'coin', 'number']; + if (!validMethods.includes(divination_method)) { + throw new AppError(`不支持的起卦方法: ${divination_method}`, 400, 'INVALID_DIVINATION_METHOD'); + } + } + + // 验证用户ID + const targetUserId = user_id || req.user.id; + if (!Number.isInteger(targetUserId) || targetUserId <= 0) { + throw new AppError('用户ID无效', 400, 'INVALID_USER_ID'); } try { // 执行易经分析(纯分析,不存储历史记录) const analysisResult = yijingAnalyzer.performYijingAnalysis({ - question: question, - user_id: user_id || req.user.id, + question: question.trim(), + user_id: targetUserId, divination_method: divination_method || 'time', user_timezone: user_timezone, local_time: local_time }); - // 只返回分析结果,不存储历史记录 - res.json({ - data: { - analysis: analysisResult - } - }); + // 使用通用响应函数 + sendAnalysisResponse(analysisResult, res); } catch (error) { console.error('易经分析详细错误:', error); console.error('错误堆栈:', error.stack); @@ -92,42 +177,30 @@ router.post('/yijing', authenticate, asyncHandler(async (req, res) => { } })); -// 紫微斗数分析接口 +/** + * 紫微斗数分析接口 + * 执行紫微斗数分析,不存储历史记录 + */ router.post('/ziwei', authenticate, asyncHandler(async (req, res) => { const { birth_data } = req.body; - // 输入验证 - if (!birth_data || !birth_data.name || !birth_data.birth_date) { - throw new AppError('缺少必要参数:姓名和出生日期', 400, 'MISSING_BIRTH_DATA'); - } + // 使用通用验证函数 + validateBirthData(birth_data); - // 验证出生日期格式 - const dateRegex = /^\d{4}-\d{2}-\d{2}$/; - if (!dateRegex.test(birth_data.birth_date)) { - throw new AppError('出生日期格式应为 YYYY-MM-DD', 400, 'INVALID_DATE_FORMAT'); - } - - // 验证出生时间格式(如果提供) - if (birth_data.birth_time) { - const timeRegex = /^\d{2}:\d{2}$/; - if (!timeRegex.test(birth_data.birth_time)) { - throw new AppError('出生时间格式应为 HH:MM', 400, 'INVALID_TIME_FORMAT'); - } + // 紫微斗数需要性别信息 + if (!birth_data.gender) { + throw new AppError('紫微斗数分析需要性别信息', 400, 'MISSING_GENDER'); } try { // 执行紫微斗数分析(纯分析,不存储历史记录) const analysisResult = ziweiAnalyzer.performRealZiweiAnalysis(birth_data); - // 只返回分析结果,不存储历史记录 - res.json({ - data: { - analysis: analysisResult - } - }); + // 使用通用响应函数 + sendAnalysisResponse(analysisResult, res); } catch (error) { console.error('紫微斗数分析错误:', error); - throw new AppError('紫微斗数分析过程中发生错误', 500, 'ZIWEI_ANALYSIS_ERROR'); + throw new AppError(`紫微斗数分析过程中发生错误: ${error.message}`, 500, 'ZIWEI_ANALYSIS_ERROR'); } })); diff --git a/server/services/baziAnalyzer.cjs b/server/services/baziAnalyzer.cjs index 54b39f2..34d5d72 100644 --- a/server/services/baziAnalyzer.cjs +++ b/server/services/baziAnalyzer.cjs @@ -52,17 +52,86 @@ class BaziAnalyzer { }; } - // 完全个性化的八字分析主函数 - 基于真实用户数据 + /** + * 完全个性化的八字分析主函数 - 基于真实用户数据 + * @param {Object} birth_data - 出生数据 + * @param {string} birth_data.birth_date - 出生日期 (YYYY-MM-DD) + * @param {string} birth_data.birth_time - 出生时间 (HH:MM) + * @param {string} birth_data.gender - 性别 ('male'/'female' 或 '男'/'女') + * @param {string} birth_data.birth_place - 出生地点(可选) + * @param {string} birth_data.name - 姓名(可选) + * @returns {Promise} 八字分析结果 + */ async performFullBaziAnalysis(birth_data) { try { + // 输入参数验证 + if (!birth_data || typeof birth_data !== 'object') { + throw new Error('输入数据无效:必须提供有效的出生数据对象'); + } + + const { birth_date, birth_time, gender, birth_place, name } = birth_data; + + // 验证出生日期 + if (!birth_date || typeof birth_date !== 'string') { + throw new Error('输入数据无效:出生日期不能为空'); + } + + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(birth_date)) { + throw new Error('输入数据无效:出生日期格式必须为 YYYY-MM-DD'); + } + + const birthDateObj = new Date(birth_date); + if (isNaN(birthDateObj.getTime())) { + throw new Error('输入数据无效:出生日期无效'); + } + + const currentDate = new Date(); + const minDate = new Date('1900-01-01'); + if (birthDateObj < minDate || birthDateObj > currentDate) { + throw new Error('输入数据无效:出生日期必须在1900年至今之间'); + } + + // 验证出生时间 + if (birth_time && typeof birth_time === 'string') { + const timeRegex = /^\d{2}:\d{2}$/; + if (!timeRegex.test(birth_time)) { + throw new Error('输入数据无效:出生时间格式必须为 HH:MM'); + } + + const [hours, minutes] = birth_time.split(':').map(Number); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + throw new Error('输入数据无效:出生时间无效'); + } + } + + // 验证性别 + if (!gender || typeof gender !== 'string') { + throw new Error('输入数据无效:性别不能为空'); + } + + const validGenders = ['male', 'female', '男', '女', '男性', '女性']; + if (!validGenders.includes(gender)) { + throw new Error('输入数据无效:性别必须是 male/female 或 男/女'); + } + + // 验证姓名长度 + if (name && (typeof name !== 'string' || name.length > 50)) { + throw new Error('输入数据无效:姓名长度不能超过50个字符'); + } + + // 验证出生地点长度 + if (birth_place && (typeof birth_place !== 'string' || birth_place.length > 100)) { + throw new Error('输入数据无效:出生地点长度不能超过100个字符'); + } + // 检查缓存 const cachedResult = this.cache.get('bazi', birth_data); if (cachedResult) { return cachedResult; } - const { birth_date, birth_time, gender, birth_place, name } = birth_data; - const personalizedName = name || '您'; + const personalizedName = (name && name.trim()) || '您'; // 1. 精确计算八字四柱(基础计算,必须先完成) const baziChart = this.calculatePreciseBazi(birth_date, birth_time); diff --git a/server/services/yijingAnalyzer.cjs b/server/services/yijingAnalyzer.cjs index 3422fa9..688daec 100644 --- a/server/services/yijingAnalyzer.cjs +++ b/server/services/yijingAnalyzer.cjs @@ -48,16 +48,49 @@ class YijingAnalyzer { }; } - // 专业易经分析主函数 + /** + * 专业易经分析主函数 + * @param {Object} inputData - 输入数据 + * @param {string} inputData.question - 占卜问题 + * @param {number} inputData.user_id - 用户ID + * @param {Object} inputData.birth_data - 出生数据(可选) + * @param {string} inputData.divination_method - 起卦方法 + * @returns {Object} 易经分析结果 + */ performYijingAnalysis(inputData) { try { + // 输入参数验证 + if (!inputData || typeof inputData !== 'object') { + throw new Error('输入数据无效:必须提供有效的输入对象'); + } + + const { question, user_id, birth_data, divination_method = 'time' } = inputData; + + // 验证必要参数 + if (!question || typeof question !== 'string' || question.trim().length === 0) { + throw new Error('输入数据无效:问题不能为空'); + } + + if (question.length > 200) { + throw new Error('输入数据无效:问题长度不能超过200个字符'); + } + + if (user_id !== undefined && (!Number.isInteger(user_id) || user_id <= 0)) { + throw new Error('输入数据无效:用户ID必须是正整数'); + } + + // 验证起卦方法 + const validMethods = ['time', 'plum_blossom', 'coin', 'number']; + if (!validMethods.includes(divination_method)) { + throw new Error(`输入数据无效:不支持的起卦方法 ${divination_method}`); + } + // 检查缓存(易经分析时效性较短,缓存时间较短) const cachedResult = this.cache.get('yijing', inputData); if (cachedResult) { return cachedResult; } - const { question, user_id, birth_data, divination_method = 'time' } = inputData; const currentTime = new Date(); // 根据不同方法起卦 @@ -74,12 +107,12 @@ class YijingAnalyzer { // 象数分析 const numerologyAnalysis = this.performNumerologyAnalysis(hexagramData, currentTime, question); - return { + const result = { analysis_type: 'yijing', analysis_date: currentTime.toISOString(), basic_info: { divination_data: { - question: question, + question: question.trim(), divination_time: currentTime.toISOString(), method: this.getMethodName(divination_method), lunar_info: this.calculateLunarInfo(currentTime) diff --git a/server/services/ziweiAnalyzer.cjs b/server/services/ziweiAnalyzer.cjs index 7759bc1..bff2c22 100644 --- a/server/services/ziweiAnalyzer.cjs +++ b/server/services/ziweiAnalyzer.cjs @@ -310,11 +310,75 @@ class ZiweiAnalyzer { return null; } - // 专业紫微斗数分析主函数 + /** + * 专业紫微斗数分析主函数 + * @param {Object} birth_data - 出生数据 + * @param {string} birth_data.birth_date - 出生日期 (YYYY-MM-DD) + * @param {string} birth_data.birth_time - 出生时间 (HH:MM) + * @param {string} birth_data.gender - 性别 ('male'/'female' 或 '男'/'女') + * @param {string} birth_data.name - 姓名(可选) + * @returns {Object} 紫微斗数分析结果 + */ performRealZiweiAnalysis(birth_data) { - const { name, birth_date, birth_time, gender } = birth_data; - const personName = name || '您'; - const personGender = gender === 'male' || gender === '男' ? '男性' : '女性'; + try { + // 输入参数验证 + if (!birth_data || typeof birth_data !== 'object') { + throw new Error('输入数据无效:必须提供有效的出生数据对象'); + } + + const { name, birth_date, birth_time, gender } = birth_data; + + // 验证出生日期 + if (!birth_date || typeof birth_date !== 'string') { + throw new Error('输入数据无效:出生日期不能为空'); + } + + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(birth_date)) { + throw new Error('输入数据无效:出生日期格式必须为 YYYY-MM-DD'); + } + + const birthDateObj = new Date(birth_date); + if (isNaN(birthDateObj.getTime())) { + throw new Error('输入数据无效:出生日期无效'); + } + + const currentDate = new Date(); + const minDate = new Date('1900-01-01'); + if (birthDateObj < minDate || birthDateObj > currentDate) { + throw new Error('输入数据无效:出生日期必须在1900年至今之间'); + } + + // 验证出生时间 + if (birth_time && typeof birth_time === 'string') { + const timeRegex = /^\d{2}:\d{2}$/; + if (!timeRegex.test(birth_time)) { + throw new Error('输入数据无效:出生时间格式必须为 HH:MM'); + } + + const [hours, minutes] = birth_time.split(':').map(Number); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + throw new Error('输入数据无效:出生时间无效'); + } + } + + // 验证性别 + if (!gender || typeof gender !== 'string') { + throw new Error('输入数据无效:性别不能为空'); + } + + const validGenders = ['male', 'female', '男', '女', '男性', '女性']; + if (!validGenders.includes(gender)) { + throw new Error('输入数据无效:性别必须是 male/female 或 男/女'); + } + + // 验证姓名长度 + if (name && (typeof name !== 'string' || name.length > 50)) { + throw new Error('输入数据无效:姓名长度不能超过50个字符'); + } + + const personName = (name && name.trim()) || '您'; + const personGender = gender === 'male' || gender === '男' ? '男性' : '女性'; // 计算精确的八字信息 const baziInfo = this.calculatePreciseBazi(birth_date, birth_time); @@ -359,6 +423,11 @@ class ZiweiAnalyzer { }, detailed_analysis: analysis }; + + } catch (error) { + console.error('紫微斗数分析详细错误:', error); + throw error; + } } // 计算五行局(紫微斗数核心算法)- 基于纳音五行 diff --git a/src/components/CompleteBaziAnalysis.tsx b/src/components/CompleteBaziAnalysis.tsx index 076dc84..8b2983b 100644 --- a/src/components/CompleteBaziAnalysis.tsx +++ b/src/components/CompleteBaziAnalysis.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, ErrorInfo } from 'react'; import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts'; -import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2, Clock, Target, Heart, DollarSign, Activity } from 'lucide-react'; +import { Calendar, Star, BookOpen, Sparkles, User, BarChart3, Zap, TrendingUp, Loader2, Clock, Target, Heart, DollarSign, Activity, AlertTriangle } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from './ui/Card'; import { BackToTop } from './ui/BackToTop'; import DownloadButton from './ui/DownloadButton'; @@ -8,22 +8,121 @@ import AIInterpretationButton from './ui/AIInterpretationButton'; import AIConfigModal from './ui/AIConfigModal'; import { localApi } from '../lib/localApi'; +/** + * 八字分析组件的Props接口 + */ interface CompleteBaziAnalysisProps { + /** 出生日期信息 */ birthDate: { + /** 出生日期 (YYYY-MM-DD) */ date: string; + /** 出生时间 (HH:MM) */ time: string; + /** 姓名(可选) */ name?: string; + /** 性别(可选) */ gender?: string; }; - analysisData?: any; // 可选的预先分析的数据 - recordId?: number; // 历史记录ID,用于AI解读 + /** 可选的预先分析的数据 */ + analysisData?: any; + /** 历史记录ID,用于AI解读 */ + recordId?: number; } +/** + * 验证出生日期数据的有效性 + * @param birthDate 出生日期数据 + * @returns 验证结果和错误信息 + */ +const validateBirthDate = (birthDate: CompleteBaziAnalysisProps['birthDate']): { isValid: boolean; error?: string } => { + if (!birthDate) { + return { isValid: false, error: '出生日期数据不能为空' }; + } + + if (!birthDate.date || typeof birthDate.date !== 'string') { + return { isValid: false, error: '出生日期不能为空' }; + } + + // 验证日期格式 + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(birthDate.date)) { + return { isValid: false, error: '出生日期格式必须为 YYYY-MM-DD' }; + } + + // 验证日期有效性 + const date = new Date(birthDate.date); + if (isNaN(date.getTime())) { + return { isValid: false, error: '出生日期无效' }; + } + + if (!birthDate.time || typeof birthDate.time !== 'string') { + return { isValid: false, error: '出生时间不能为空' }; + } + + // 验证时间格式 + const timeRegex = /^\d{2}:\d{2}$/; + if (!timeRegex.test(birthDate.time)) { + return { isValid: false, error: '出生时间格式必须为 HH:MM' }; + } + + // 验证时间有效性 + const [hours, minutes] = birthDate.time.split(':').map(Number); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { + return { isValid: false, error: '出生时间无效' }; + } + + // 验证姓名长度 + if (birthDate.name && birthDate.name.length > 50) { + return { isValid: false, error: '姓名长度不能超过50个字符' }; + } + + // 验证性别 + if (birthDate.gender) { + const validGenders = ['male', 'female', '男', '女']; + if (!validGenders.includes(birthDate.gender)) { + return { isValid: false, error: '性别必须是 male/female 或 男/女' }; + } + } + + return { isValid: true }; +}; + +/** + * 错误显示组件 + */ +const ErrorDisplay: React.FC<{ error: string; onRetry?: () => void }> = ({ error, onRetry }) => ( + + +
+ +
+

分析出错

+

{error}

+ {onRetry && ( + + )} +
+
+
+
+); + const CompleteBaziAnalysis: React.FC = ({ birthDate, analysisData: propAnalysisData, recordId }) => { const [isLoading, setIsLoading] = useState(!propAnalysisData); const [error, setError] = useState(null); const [analysisData, setAnalysisData] = useState(propAnalysisData || null); const [showAIConfig, setShowAIConfig] = useState(false); + + // 输入验证 + const validation = validateBirthDate(birthDate); + if (!validation.isValid) { + return ; + } // 五行颜色配置 const elementColors: { [key: string]: string } = { @@ -58,6 +157,40 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate, '日主': 'bg-amber-100 text-amber-800 border-amber-300' }; + // 分析数据获取函数 + const fetchAnalysisData = async () => { + try { + setIsLoading(true); + setError(null); + + const birthData = { + name: birthDate.name || '用户', + birth_date: birthDate.date, + birth_time: birthDate.time, + gender: birthDate.gender || 'male' + }; + + const baziResponse = await localApi.analysis.bazi(birthData); + + if (baziResponse.error) { + throw new Error(baziResponse.error.message || '八字分析失败'); + } + + const analysisResult = baziResponse.data?.analysis; + if (!analysisResult) { + throw new Error('分析结果为空'); + } + + setAnalysisData(analysisResult); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '分析数据获取失败,请稍后重试'; + console.error('八字分析错误:', err); + setError(errorMessage); + } finally { + setIsLoading(false); + } + }; + useEffect(() => { // 如果已经有分析数据,直接使用 if (propAnalysisData) { @@ -66,37 +199,6 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate, return; } - const fetchAnalysisData = async () => { - try { - setIsLoading(true); - setError(null); - - const birthData = { - name: birthDate.name || '用户', - birth_date: birthDate.date, - birth_time: birthDate.time, - gender: birthDate.gender || 'male' - }; - - const baziResponse = await localApi.analysis.bazi(birthData); - - if (baziResponse.error) { - throw new Error(baziResponse.error.message || '八字分析失败'); - } - - const analysisResult = baziResponse.data?.analysis; - if (!analysisResult) { - throw new Error('分析结果为空'); - } - - setAnalysisData(analysisResult); - } catch (err) { - setError(err instanceof Error ? err.message : '分析数据获取失败,请稍后重试'); - } finally { - setIsLoading(false); - } - }; - if (birthDate?.date && !propAnalysisData) { fetchAnalysisData(); } @@ -119,6 +221,11 @@ const CompleteBaziAnalysis: React.FC = ({ birthDate, // 渲染错误状态 if (error) { + return ; + } + + // 检查分析数据的完整性 + if (!analysisData) { return (