Commit d7c7a020 by Performance System

🚀 v8.6: 新增重复图片检测和历史统计功能

 新功能:
- 重复图片检测: 智能检测重复图片上传,支持完全重复/轻微编辑/同名不同内容等情况
- 历史统计功能: 管理员可保存和查看历史月份的用户绩效数据

🔧 技术实现:
- calculateImageHash(): 基于图片内容计算哈希值
- detectDuplicateImage(): 智能重复检测逻辑
- saveCurrentMonthStats(): 保存月度统计数据
- getHistoryStats(): 历史数据管理

🎨 界面优化:
- 新增历史统计标签页
- 月份筛选和数据展示
- 重复图片检测提示优化

📝 文档更新:
- 新功能演示指南
- 测试脚本和诊断工具
- 修复总结文档更新

🧪 测试工具:
- test-new-features.js: 功能测试脚本
- diagnose.html: 系统诊断页面
parent 908ffa66
/**
* 诊断图片上传问题的脚本
* 检查机构ID、权限验证和数据一致性
*/
// 在浏览器控制台中运行此脚本
function diagnoseUploadIssue() {
console.log('🔍 开始诊断图片上传问题...')
// 1. 检查localStorage中的数据
console.log('\n1️⃣ 检查localStorage数据...')
const institutionsData = localStorage.getItem('score_system_institutions')
const usersData = localStorage.getItem('score_system_users')
if (!institutionsData || !usersData) {
console.error('❌ localStorage中没有找到数据')
return
}
const institutions = JSON.parse(institutionsData)
const users = JSON.parse(usersData)
console.log(`找到 ${institutions.length} 个机构,${users.length} 个用户`)
// 2. 检查机构数据结构
console.log('\n2️⃣ 检查机构数据结构...')
institutions.forEach((inst, index) => {
console.log(`机构 ${index + 1}: ${inst.name}`)
console.log(` - 内部ID: ${inst.id}`)
console.log(` - 机构编号: ${inst.institutionId}`)
console.log(` - 负责人ID: ${inst.ownerId}`)
console.log(` - 图片数量: ${inst.images ? inst.images.length : 0}`)
// 检查数据完整性
const issues = []
if (!inst.id) issues.push('缺少内部ID')
if (!inst.institutionId) issues.push('缺少机构编号')
if (!inst.ownerId) issues.push('缺少负责人ID')
if (!inst.images) issues.push('缺少images数组')
if (issues.length > 0) {
console.warn(` ⚠️ 数据问题: ${issues.join(', ')}`)
}
console.log(' ---')
})
// 3. 检查机构ID重复
console.log('\n3️⃣ 检查机构ID重复...')
const institutionIdMap = new Map()
const internalIdMap = new Map()
institutions.forEach(inst => {
// 检查机构编号重复
if (inst.institutionId) {
if (institutionIdMap.has(inst.institutionId)) {
const existing = institutionIdMap.get(inst.institutionId)
console.error(`🚨 发现重复机构编号: ${inst.institutionId}`)
console.error(` - 机构1: ${existing.name}`)
console.error(` - 机构2: ${inst.name}`)
} else {
institutionIdMap.set(inst.institutionId, inst)
}
}
// 检查内部ID重复
if (inst.id) {
if (internalIdMap.has(inst.id)) {
const existing = internalIdMap.get(inst.id)
console.error(`🚨 发现重复内部ID: ${inst.id}`)
console.error(` - 机构1: ${existing.name}`)
console.error(` - 机构2: ${inst.name}`)
} else {
internalIdMap.set(inst.id, inst)
}
}
})
// 4. 检查用户权限映射
console.log('\n4️⃣ 检查用户权限映射...')
users.forEach(user => {
if (user.role === 'user') {
const userInstitutions = institutions.filter(inst => inst.ownerId === user.id)
console.log(`用户 ${user.name} (${user.id}) 负责 ${userInstitutions.length} 个机构:`)
userInstitutions.forEach(inst => {
console.log(` - ${inst.name} (编号: ${inst.institutionId}, 内部ID: ${inst.id})`)
})
if (userInstitutions.length === 0) {
console.warn(` ⚠️ 用户 ${user.name} 没有负责任何机构`)
}
}
})
// 5. 检查特定问题机构
console.log('\n5️⃣ 检查特定问题机构...')
const problemKeywords = [
'五华区长青口腔诊所',
'昆明市五华区爱雅仕口腔诊所',
'昆明美云口腔医院有限公司安宁口腔诊所',
'兰州至善振林康美口腔医疗有限责任公司'
]
problemKeywords.forEach(keyword => {
const matchingInsts = institutions.filter(inst =>
inst.name.includes(keyword) || inst.name.includes(keyword.split('口腔')[0])
)
if (matchingInsts.length > 0) {
console.log(`关键词 "${keyword}" 匹配的机构:`)
matchingInsts.forEach(inst => {
const owner = users.find(u => u.id === inst.ownerId)
console.log(` - ${inst.name}`)
console.log(` 编号: ${inst.institutionId}, 内部ID: ${inst.id}`)
console.log(` 负责人: ${owner ? owner.name : '未知'} (${inst.ownerId})`)
console.log(` 图片数量: ${inst.images ? inst.images.length : 0}`)
})
} else {
console.log(`关键词 "${keyword}" 没有匹配的机构`)
}
})
// 6. 模拟图片上传权限验证
console.log('\n6️⃣ 模拟图片上传权限验证...')
users.forEach(user => {
if (user.role === 'user') {
const userInstitutions = institutions.filter(inst => inst.ownerId === user.id)
console.log(`测试用户 ${user.name} 的上传权限:`)
userInstitutions.forEach(inst => {
// 模拟权限验证逻辑
const canUpload = (
user.id && // 用户ID存在
inst.id && // 机构内部ID存在
inst.ownerId === user.id // 权限匹配
)
const status = canUpload ? '✅' : '❌'
console.log(` ${status} ${inst.name} (内部ID: ${inst.id})`)
if (!canUpload) {
console.log(` 问题: 用户ID(${user.id}) vs 机构负责人(${inst.ownerId})`)
}
})
}
})
// 7. 生成修复建议
console.log('\n7️⃣ 修复建议...')
const suggestions = []
// 检查是否有重复ID
if (institutionIdMap.size < institutions.length) {
suggestions.push('存在重复的机构编号,建议执行"修复权限验证"')
}
if (internalIdMap.size < institutions.length) {
suggestions.push('存在重复的内部ID,建议执行"修复权限验证"')
}
// 检查是否有无负责人的机构
const orphanInstitutions = institutions.filter(inst => !inst.ownerId)
if (orphanInstitutions.length > 0) {
suggestions.push(`有 ${orphanInstitutions.length} 个机构没有负责人,需要分配负责人`)
}
// 检查是否有无机构的用户
const usersWithoutInstitutions = users.filter(user =>
user.role === 'user' &&
!institutions.some(inst => inst.ownerId === user.id)
)
if (usersWithoutInstitutions.length > 0) {
suggestions.push(`有 ${usersWithoutInstitutions.length} 个用户没有负责任何机构`)
}
if (suggestions.length > 0) {
console.log('建议的修复操作:')
suggestions.forEach((suggestion, index) => {
console.log(`${index + 1}. ${suggestion}`)
})
} else {
console.log('✅ 没有发现明显的数据问题')
}
return {
institutions: institutions.length,
users: users.length,
duplicateInstitutionIds: institutionIdMap.size < institutions.length,
duplicateInternalIds: internalIdMap.size < institutions.length,
orphanInstitutions: orphanInstitutions.length,
usersWithoutInstitutions: usersWithoutInstitutions.length,
suggestions
}
}
// 检查图片上传具体错误的函数
function checkUploadError(userId, institutionInternalId) {
console.log(`🔍 检查用户 ${userId} 上传到机构 ${institutionInternalId} 的权限...`)
const institutionsData = localStorage.getItem('score_system_institutions')
const usersData = localStorage.getItem('score_system_users')
if (!institutionsData || !usersData) {
console.error('❌ 无法获取数据')
return false
}
const institutions = JSON.parse(institutionsData)
const users = JSON.parse(usersData)
const user = users.find(u => u.id === userId)
const institution = institutions.find(i => i.id === institutionInternalId)
console.log('用户信息:', user)
console.log('机构信息:', institution)
if (!user) {
console.error('❌ 用户不存在')
return false
}
if (!institution) {
console.error('❌ 机构不存在')
return false
}
if (institution.ownerId !== userId) {
console.error(`❌ 权限不匹配: 机构负责人(${institution.ownerId}) != 当前用户(${userId})`)
return false
}
console.log('✅ 权限验证通过')
return true
}
// 导出函数供浏览器使用
if (typeof window !== 'undefined') {
window.diagnoseUploadIssue = diagnoseUploadIssue
window.checkUploadError = checkUploadError
console.log('诊断脚本已加载!')
console.log('使用方法:')
console.log('1. diagnoseUploadIssue() - 全面诊断系统数据')
console.log('2. checkUploadError(userId, institutionId) - 检查特定上传权限')
}
/**
* 数据清理脚本
* 在浏览器控制台中运行此脚本来清理数据
*/
// 清理所有非管理员数据
function cleanupNonAdminData() {
try {
console.log('开始清理数据...');
// 获取当前数据
const usersData = localStorage.getItem('score_system_users');
const institutionsData = localStorage.getItem('score_system_institutions');
if (usersData) {
const users = JSON.parse(usersData);
console.log('清理前用户数量:', users.length);
// 只保留管理员用户
const adminUsers = users.filter(user => user.role === 'admin');
if (adminUsers.length === 0) {
// 如果没有管理员,创建默认管理员
adminUsers.push({
id: 'admin',
name: '系统管理员',
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
});
}
localStorage.setItem('score_system_users', JSON.stringify(adminUsers));
console.log('清理后用户数量:', adminUsers.length);
}
if (institutionsData) {
const institutions = JSON.parse(institutionsData);
console.log('清理前机构数量:', institutions.length);
// 清空所有机构
localStorage.setItem('score_system_institutions', JSON.stringify([]));
console.log('清理后机构数量: 0');
}
// 清理当前用户会话(如果不是管理员)
const currentUserData = localStorage.getItem('score_system_current_user');
if (currentUserData) {
const currentUser = JSON.parse(currentUserData);
if (currentUser.role !== 'admin') {
localStorage.removeItem('score_system_current_user');
console.log('已清理非管理员用户会话');
}
}
console.log('数据清理完成!');
console.log('请刷新页面以查看效果。');
return true;
} catch (error) {
console.error('数据清理失败:', error);
return false;
}
}
// 验证数据完整性
function validateData() {
try {
const usersData = localStorage.getItem('score_system_users');
const institutionsData = localStorage.getItem('score_system_institutions');
const users = usersData ? JSON.parse(usersData) : [];
const institutions = institutionsData ? JSON.parse(institutionsData) : [];
const report = {
totalUsers: users.length,
adminUsers: users.filter(u => u.role === 'admin').length,
regularUsers: users.filter(u => u.role === 'user').length,
totalInstitutions: institutions.length,
orphanedInstitutions: institutions.filter(inst => {
return !users.some(user => user.institutions && user.institutions.includes(inst.institutionId));
}).length
};
console.log('=== 数据完整性报告 ===');
console.log('总用户数:', report.totalUsers);
console.log('管理员用户:', report.adminUsers);
console.log('普通用户:', report.regularUsers);
console.log('总机构数:', report.totalInstitutions);
console.log('孤立机构:', report.orphanedInstitutions);
console.log('=====================');
return report;
} catch (error) {
console.error('数据验证失败:', error);
return null;
}
}
// 导出数据
function exportData() {
try {
const data = {
users: JSON.parse(localStorage.getItem('score_system_users') || '[]'),
institutions: JSON.parse(localStorage.getItem('score_system_institutions') || '[]'),
config: JSON.parse(localStorage.getItem('score_system_config') || '{}'),
exportTime: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `score_system_backup_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('数据导出完成');
return true;
} catch (error) {
console.error('数据导出失败:', error);
return false;
}
}
// 重置系统
function resetSystem() {
try {
if (confirm('此操作将重置整个系统,删除所有数据。确定要继续吗?')) {
localStorage.clear();
sessionStorage.clear();
console.log('系统已重置');
console.log('请刷新页面');
return true;
}
return false;
} catch (error) {
console.error('系统重置失败:', error);
return false;
}
}
// 显示帮助信息
function showHelp() {
console.log('=== 数据清理脚本帮助 ===');
console.log('可用命令:');
console.log('cleanupNonAdminData() - 清理所有非管理员数据');
console.log('validateData() - 验证数据完整性');
console.log('exportData() - 导出当前数据');
console.log('resetSystem() - 重置整个系统');
console.log('showHelp() - 显示此帮助信息');
console.log('========================');
}
// 自动显示帮助信息
console.log('数据清理脚本已加载');
showHelp();
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>跨浏览器数据同步测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.status {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #ccc;
}
.status-indicator.connected {
background-color: #4CAF50;
}
.status-indicator.disconnected {
background-color: #f44336;
}
.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: black;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.data-display {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.data-section {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
}
.data-section h3 {
margin-top: 0;
color: #333;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin-bottom: 5px;
padding: 2px 5px;
border-radius: 2px;
}
.log-success {
background-color: #d4edda;
color: #155724;
}
.log-error {
background-color: #f8d7da;
color: #721c24;
}
.log-warning {
background-color: #fff3cd;
color: #856404;
}
.log-info {
background-color: #d1ecf1;
color: #0c5460;
}
.browser-info {
background-color: #e9ecef;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>跨浏览器数据同步测试</h1>
<div class="browser-info">
<strong>浏览器信息:</strong> <span id="browserInfo"></span><br>
<strong>测试说明:</strong> 在不同浏览器中打开此页面,测试数据是否能够跨浏览器同步
</div>
<div class="status">
<div class="status-indicator" id="serverStatus"></div>
<span id="serverText">服务器未连接</span>
<span>|</span>
<span>用户数: <span id="userCount">0</span></span>
<span>|</span>
<span>机构数: <span id="institutionCount">0</span></span>
</div>
<div class="button-group">
<button class="btn-primary" onclick="checkServerConnection()">检查服务器连接</button>
<button class="btn-success" onclick="loadFromServer()">从服务器加载数据</button>
<button class="btn-warning" onclick="saveToServer()">保存到服务器</button>
<button class="btn-success" onclick="testAddUser()">添加测试用户</button>
<button class="btn-success" onclick="testAddInstitution()">添加测试机构</button>
<button class="btn-danger" onclick="clearLocalData()">清空本地数据</button>
<button class="btn-primary" onclick="refreshData()">刷新显示</button>
</div>
</div>
<div class="data-display">
<div class="container">
<div class="data-section">
<h3>用户数据</h3>
<div id="userData"></div>
</div>
</div>
<div class="container">
<div class="data-section">
<h3>机构数据</h3>
<div id="institutionData"></div>
</div>
</div>
</div>
<div class="container">
<h3>操作日志</h3>
<div class="log" id="logContainer"></div>
</div>
<script>
const serverUrl = 'http://localhost:3000';
function log(message, type = 'info') {
const logContainer = document.getElementById('logContainer');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateServerStatus(connected) {
const indicator = document.getElementById('serverStatus');
const text = document.getElementById('serverText');
if (connected) {
indicator.className = 'status-indicator connected';
text.textContent = '服务器已连接';
} else {
indicator.className = 'status-indicator disconnected';
text.textContent = '服务器未连接';
}
}
function updateDataDisplay() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
document.getElementById('userCount').textContent = users.length;
document.getElementById('institutionCount').textContent = institutions.length;
// 显示用户数据
const userData = document.getElementById('userData');
userData.innerHTML = users.map(user =>
`<div><strong>${user.name}</strong> (${user.phone}) - ${user.role}</div>`
).join('') || '<div>暂无用户数据</div>';
// 显示机构数据
const institutionData = document.getElementById('institutionData');
institutionData.innerHTML = institutions.map(inst =>
`<div><strong>${inst.name}</strong> (ID: ${inst.institutionId})</div>`
).join('') || '<div>暂无机构数据</div>';
} catch (error) {
log(`更新数据显示失败: ${error.message}`, 'error');
}
}
async function checkServerConnection() {
try {
const response = await fetch(`${serverUrl}/health`);
if (response.ok) {
updateServerStatus(true);
log('服务器连接正常', 'success');
return true;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
updateServerStatus(false);
log(`服务器连接失败: ${error.message}`, 'error');
return false;
}
}
async function loadFromServer() {
try {
log('正在从服务器加载数据...', 'info');
const response = await fetch(`${serverUrl}/api/system-data`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const systemData = await response.json();
log('从服务器获取到数据: ' + JSON.stringify(systemData).substring(0, 100) + '...', 'info');
// 保存到localStorage
if (systemData.users) {
localStorage.setItem('score_system_users', JSON.stringify(systemData.users));
}
if (systemData.institutions) {
localStorage.setItem('score_system_institutions', JSON.stringify(systemData.institutions));
}
if (systemData.systemConfig) {
localStorage.setItem('score_system_config', JSON.stringify(systemData.systemConfig));
}
updateDataDisplay();
log('数据已从服务器同步到本地', 'success');
} catch (error) {
log(`从服务器加载数据失败: ${error.message}`, 'error');
}
}
async function saveToServer() {
try {
log('正在保存数据到服务器...', 'info');
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const systemConfig = JSON.parse(localStorage.getItem('score_system_config') || '{}');
const systemData = { users, institutions, systemConfig };
const response = await fetch(`${serverUrl}/api/system-data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(systemData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
log('数据已保存到服务器', 'success');
} catch (error) {
log(`保存数据到服务器失败: ${error.message}`, 'error');
}
}
function testAddUser() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const newUser = {
id: 'test_user_' + Date.now(),
name: '测试用户' + (users.length + 1),
phone: '1380000' + String(users.length + 1).padStart(4, '0'),
password: '123456',
role: 'user',
institutions: []
};
users.push(newUser);
localStorage.setItem('score_system_users', JSON.stringify(users));
updateDataDisplay();
log(`添加测试用户: ${newUser.name}`, 'success');
// 自动保存到服务器
saveToServer();
} catch (error) {
log(`添加用户失败: ${error.message}`, 'error');
}
}
function testAddInstitution() {
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const newInstitution = {
id: 'test_inst_' + Date.now(),
institutionId: String(institutions.length + 1).padStart(3, '0'),
name: '测试机构' + (institutions.length + 1),
ownerId: null,
images: []
};
institutions.push(newInstitution);
localStorage.setItem('score_system_institutions', JSON.stringify(institutions));
updateDataDisplay();
log(`添加测试机构: ${newInstitution.name}`, 'success');
// 自动保存到服务器
saveToServer();
} catch (error) {
log(`添加机构失败: ${error.message}`, 'error');
}
}
function clearLocalData() {
try {
// 只保留管理员用户
const adminUser = {
id: 'admin',
name: '系统管理员',
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
};
localStorage.setItem('score_system_users', JSON.stringify([adminUser]));
localStorage.setItem('score_system_institutions', JSON.stringify([]));
updateDataDisplay();
log('本地数据已清空,只保留管理员', 'warning');
} catch (error) {
log(`清空数据失败: ${error.message}`, 'error');
}
}
function refreshData() {
updateDataDisplay();
log('数据显示已刷新', 'info');
}
// 页面加载时初始化
window.addEventListener('load', async () => {
// 显示浏览器信息
document.getElementById('browserInfo').textContent = navigator.userAgent;
// 检查服务器连接
await checkServerConnection();
// 更新数据显示
updateDataDisplay();
log('页面加载完成', 'info');
log('请在不同浏览器中打开此页面测试跨浏览器同步', 'info');
});
// 定期检查服务器连接和同步数据
setInterval(async () => {
const isConnected = await checkServerConnection();
if (isConnected) {
await loadFromServer();
}
}, 10000); // 每10秒检查一次
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据同步测试</title>
<script src="/socket.io/socket.io.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.status {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #ccc;
}
.status-indicator.connected {
background-color: #4CAF50;
}
.status-indicator.disconnected {
background-color: #f44336;
}
.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: black;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.data-display {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.data-section {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
}
.data-section h3 {
margin-top: 0;
color: #333;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin-bottom: 5px;
padding: 2px 5px;
border-radius: 2px;
}
.log-success {
background-color: #d4edda;
color: #155724;
}
.log-error {
background-color: #f8d7da;
color: #721c24;
}
.log-warning {
background-color: #fff3cd;
color: #856404;
}
.log-info {
background-color: #d1ecf1;
color: #0c5460;
}
</style>
</head>
<body>
<div class="container">
<h1>数据同步测试工具</h1>
<div class="status">
<div class="status-indicator" id="connectionStatus"></div>
<span id="connectionText">未连接</span>
<span>|</span>
<span>在线用户: <span id="onlineCount">0</span></span>
<span>|</span>
<span>用户数: <span id="userCount">0</span></span>
<span>|</span>
<span>机构数: <span id="institutionCount">0</span></span>
</div>
<div class="button-group">
<button class="btn-primary" onclick="connectWebSocket()">连接WebSocket</button>
<button class="btn-warning" onclick="disconnectWebSocket()">断开连接</button>
<button class="btn-success" onclick="testAddUser()">测试添加用户</button>
<button class="btn-success" onclick="testAddInstitution()">测试添加机构</button>
<button class="btn-danger" onclick="clearData()">清空测试数据</button>
<button class="btn-primary" onclick="refreshData()">刷新数据</button>
</div>
</div>
<div class="data-display">
<div class="container">
<div class="data-section">
<h3>用户数据</h3>
<div id="userData"></div>
</div>
</div>
<div class="container">
<div class="data-section">
<h3>机构数据</h3>
<div id="institutionData"></div>
</div>
</div>
</div>
<div class="container">
<h3>操作日志</h3>
<div class="log" id="logContainer"></div>
</div>
<script>
let socket = null;
function log(message, type = 'info') {
const logContainer = document.getElementById('logContainer');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateConnectionStatus(connected) {
const indicator = document.getElementById('connectionStatus');
const text = document.getElementById('connectionText');
if (connected) {
indicator.className = 'status-indicator connected';
text.textContent = '已连接';
} else {
indicator.className = 'status-indicator disconnected';
text.textContent = '未连接';
}
}
function updateDataDisplay() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
document.getElementById('userCount').textContent = users.length;
document.getElementById('institutionCount').textContent = institutions.length;
// 显示用户数据
const userData = document.getElementById('userData');
userData.innerHTML = users.map(user =>
`<div><strong>${user.name}</strong> (${user.phone}) - ${user.role}</div>`
).join('');
// 显示机构数据
const institutionData = document.getElementById('institutionData');
institutionData.innerHTML = institutions.map(inst =>
`<div><strong>${inst.name}</strong> (ID: ${inst.institutionId})</div>`
).join('');
} catch (error) {
log(`更新数据显示失败: ${error.message}`, 'error');
}
}
function connectWebSocket() {
try {
socket = io('http://localhost:3000');
socket.on('connect', () => {
updateConnectionStatus(true);
log('WebSocket连接成功', 'success');
// 登录到同步系统
socket.emit('user_login', {
id: 'test_user_' + Date.now(),
name: '测试用户',
role: 'user',
department: '测试部门',
browserInfo: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
}
});
});
socket.on('disconnect', () => {
updateConnectionStatus(false);
log('WebSocket连接断开', 'warning');
});
socket.on('connect_error', (error) => {
log(`连接错误: ${error.message}`, 'error');
});
socket.on('data_changed', (data) => {
log(`收到数据变更: ${data.change?.type || '未知类型'}`, 'info');
if (data.change && ['user_added', 'institution_added', 'data_saved'].includes(data.change.type)) {
setTimeout(updateDataDisplay, 100);
}
});
socket.on('online_users', (users) => {
document.getElementById('onlineCount').textContent = users.length;
log(`在线用户更新: ${users.length}人`, 'info');
});
socket.on('login_success', (data) => {
log(`登录成功: ${data.userInfo.name}`, 'success');
});
} catch (error) {
log(`连接失败: ${error.message}`, 'error');
}
}
function disconnectWebSocket() {
if (socket) {
socket.disconnect();
socket = null;
updateConnectionStatus(false);
log('手动断开WebSocket连接', 'warning');
}
}
function testAddUser() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const newUser = {
id: 'test_user_' + Date.now(),
name: '测试用户' + (users.length + 1),
phone: '1380000' + String(users.length + 1).padStart(4, '0'),
password: '123456',
role: 'user',
institutions: []
};
users.push(newUser);
localStorage.setItem('score_system_users', JSON.stringify(users));
// 触发数据变更事件
if (socket && socket.connected) {
socket.emit('data_update', {
type: 'user_added',
data: { user: newUser, totalUsers: users.length },
timestamp: new Date()
});
}
updateDataDisplay();
log(`测试添加用户: ${newUser.name}`, 'success');
} catch (error) {
log(`添加用户失败: ${error.message}`, 'error');
}
}
function testAddInstitution() {
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const newInstitution = {
id: 'test_inst_' + Date.now(),
institutionId: String(institutions.length + 1).padStart(3, '0'),
name: '测试机构' + (institutions.length + 1),
ownerId: null,
images: []
};
institutions.push(newInstitution);
localStorage.setItem('score_system_institutions', JSON.stringify(institutions));
// 触发数据变更事件
if (socket && socket.connected) {
socket.emit('data_update', {
type: 'institution_added',
data: { institution: newInstitution, totalInstitutions: institutions.length },
timestamp: new Date()
});
}
updateDataDisplay();
log(`测试添加机构: ${newInstitution.name}`, 'success');
} catch (error) {
log(`添加机构失败: ${error.message}`, 'error');
}
}
function clearData() {
try {
// 只保留管理员用户
const adminUser = {
id: 'admin',
name: '系统管理员',
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
};
localStorage.setItem('score_system_users', JSON.stringify([adminUser]));
localStorage.setItem('score_system_institutions', JSON.stringify([]));
updateDataDisplay();
log('测试数据已清空,只保留管理员', 'warning');
} catch (error) {
log(`清空数据失败: ${error.message}`, 'error');
}
}
function refreshData() {
updateDataDisplay();
log('手动刷新数据', 'info');
}
// 页面加载时初始化
window.addEventListener('load', () => {
updateDataDisplay();
log('页面加载完成', 'info');
});
// 监听localStorage变化
window.addEventListener('storage', (event) => {
if (event.key && event.key.startsWith('score_system_')) {
log(`检测到localStorage变化: ${event.key}`, 'info');
updateDataDisplay();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>绩效计分系统 - 图片上传问题诊断</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
h1 {
color: #409eff;
text-align: center;
margin-bottom: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 6px;
}
.section h2 {
color: #303133;
margin-top: 0;
border-bottom: 2px solid #409eff;
padding-bottom: 10px;
}
.button {
background: #409eff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
font-size: 14px;
}
.button:hover {
background: #66b1ff;
}
.button.warning {
background: #e6a23c;
}
.button.warning:hover {
background: #ebb563;
}
.button.success {
background: #67c23a;
}
.button.success:hover {
background: #85ce61;
}
.result {
margin-top: 15px;
padding: 15px;
border-radius: 4px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
}
.result.success {
background: #f0f9ff;
border: 1px solid #409eff;
color: #409eff;
}
.result.warning {
background: #fdf6ec;
border: 1px solid #e6a23c;
color: #e6a23c;
}
.result.error {
background: #fef0f0;
border: 1px solid #f56c6c;
color: #f56c6c;
}
.input-group {
margin: 10px 0;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #606266;
}
.input-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
}
.tips {
background: #f4f4f5;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #409eff;
}
.tips h3 {
margin-top: 0;
color: #409eff;
}
</style>
</head>
<body>
<div class="container">
<h1>🔍 绩效计分系统 - 图片上传问题诊断</h1>
<div class="tips">
<h3>使用说明</h3>
<p>1. 请先确保主系统正在运行(http://localhost:5174/)</p>
<p>2. 点击下方按钮进行系统诊断</p>
<p>3. 如果发现问题,请使用管理员账户登录主系统进行修复</p>
<p>4. 管理员账户:用户名 admin,密码 admin123</p>
</div>
<div class="section">
<h2>🔧 系统诊断</h2>
<button class="button" onclick="checkSystemStatus()">检查系统状态</button>
<button class="button warning" onclick="diagnoseFullSystem()">全面系统诊断</button>
<button class="button success" onclick="checkDataStructure()">检查数据结构</button>
<div id="systemResult" class="result" style="display: none;"></div>
</div>
<div class="section">
<h2>🔍 权限诊断</h2>
<div class="input-group">
<label>用户ID:</label>
<input type="text" id="userId" placeholder="输入用户ID,如:user_123456">
</div>
<div class="input-group">
<label>机构内部ID:</label>
<input type="text" id="institutionId" placeholder="输入机构内部ID,如:inst_123456_abc">
</div>
<button class="button" onclick="checkUploadPermission()">检查上传权限</button>
<div id="permissionResult" class="result" style="display: none;"></div>
</div>
<div class="section">
<h2>📊 数据统计</h2>
<button class="button" onclick="showDataStats()">显示数据统计</button>
<button class="button warning" onclick="listAllInstitutions()">列出所有机构</button>
<button class="button success" onclick="listAllUsers()">列出所有用户</button>
<div id="statsResult" class="result" style="display: none;"></div>
</div>
<div class="section">
<h2>🚀 快速修复</h2>
<p style="color: #e6a23c;">⚠️ 以下操作需要管理员权限,请在主系统中执行</p>
<button class="button warning" onclick="showFixInstructions()">显示修复指南</button>
<div id="fixResult" class="result" style="display: none;"></div>
</div>
</div>
<script>
// 检查系统状态
function checkSystemStatus() {
const result = document.getElementById('systemResult');
result.style.display = 'block';
result.className = 'result success';
try {
const institutions = localStorage.getItem('score_system_institutions');
const users = localStorage.getItem('score_system_users');
if (!institutions || !users) {
result.className = 'result error';
result.textContent = '❌ 系统数据未找到\n请确保主系统已启动并初始化';
return;
}
const instData = JSON.parse(institutions);
const userData = JSON.parse(users);
result.textContent = `✅ 系统状态正常
📊 数据统计:
- 机构数量:${instData.length}
- 用户数量:${userData.length}
- 数据最后更新:${new Date().toLocaleString()}
🔗 主系统地址:http://localhost:5174/`;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 检查系统状态失败:${error.message}`;
}
}
// 全面系统诊断
function diagnoseFullSystem() {
const result = document.getElementById('systemResult');
result.style.display = 'block';
result.className = 'result warning';
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
let report = '🔍 全面系统诊断报告\n\n';
let issues = 0;
// 检查机构数据
report += '1️⃣ 机构数据检查:\n';
institutions.forEach((inst, index) => {
const problems = [];
if (!inst.id) problems.push('缺少内部ID');
if (!inst.institutionId) problems.push('缺少机构编号');
if (!inst.name) problems.push('缺少机构名称');
if (inst.ownerId === undefined) problems.push('缺少负责人ID');
if (!inst.images) problems.push('缺少images数组');
if (problems.length > 0) {
report += ` ❌ 机构${index + 1} "${inst.name}": ${problems.join(', ')}\n`;
issues++;
}
});
if (issues === 0) {
report += ' ✅ 所有机构数据结构正常\n';
}
// 检查用户数据
report += '\n2️⃣ 用户数据检查:\n';
const normalUsers = users.filter(u => u.role === 'user');
normalUsers.forEach(user => {
const userInsts = institutions.filter(i => i.ownerId === user.id);
report += ` 👤 ${user.name}: 负责 ${userInsts.length} 个机构\n`;
});
// 检查ID重复
report += '\n3️⃣ ID重复检查:\n';
const instIds = institutions.map(i => i.institutionId).filter(Boolean);
const internalIds = institutions.map(i => i.id).filter(Boolean);
const duplicateInstIds = instIds.filter((id, index) => instIds.indexOf(id) !== index);
const duplicateInternalIds = internalIds.filter((id, index) => internalIds.indexOf(id) !== index);
if (duplicateInstIds.length > 0) {
report += ` ❌ 发现重复机构编号: ${duplicateInstIds.join(', ')}\n`;
issues++;
}
if (duplicateInternalIds.length > 0) {
report += ` ❌ 发现重复内部ID: ${duplicateInternalIds.join(', ')}\n`;
issues++;
}
if (duplicateInstIds.length === 0 && duplicateInternalIds.length === 0) {
report += ' ✅ 没有发现重复ID\n';
}
// 总结
report += `\n📊 诊断总结:\n`;
if (issues === 0) {
report += '✅ 系统状态良好,没有发现问题';
result.className = 'result success';
} else {
report += `❌ 发现 ${issues} 个问题,建议执行修复操作`;
result.className = 'result error';
}
result.textContent = report;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 诊断失败:${error.message}`;
}
}
// 检查数据结构
function checkDataStructure() {
const result = document.getElementById('systemResult');
result.style.display = 'block';
result.className = 'result success';
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
let report = '📋 数据结构检查报告\n\n';
institutions.forEach((inst, index) => {
report += `机构 ${index + 1}: ${inst.name}\n`;
report += ` - 内部ID: ${inst.id || '❌ 缺失'}\n`;
report += ` - 机构编号: ${inst.institutionId || '❌ 缺失'}\n`;
report += ` - 负责人: ${inst.ownerId || '❌ 缺失'}\n`;
report += ` - 图片数组: ${inst.images ? `✅ (${inst.images.length}张)` : '❌ 缺失'}\n`;
report += '\n';
});
result.textContent = report;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 检查数据结构失败:${error.message}`;
}
}
// 检查上传权限
function checkUploadPermission() {
const userId = document.getElementById('userId').value.trim();
const institutionId = document.getElementById('institutionId').value.trim();
const result = document.getElementById('permissionResult');
result.style.display = 'block';
if (!userId || !institutionId) {
result.className = 'result warning';
result.textContent = '⚠️ 请输入用户ID和机构内部ID';
return;
}
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const user = users.find(u => u.id === userId);
const institution = institutions.find(i => i.id === institutionId);
let report = `🔍 权限检查报告\n\n`;
report += `用户ID: ${userId}\n`;
report += `机构ID: ${institutionId}\n\n`;
if (!user) {
result.className = 'result error';
report += '❌ 用户不存在\n';
report += '建议:检查用户ID是否正确';
} else if (!institution) {
result.className = 'result error';
report += '❌ 机构不存在\n';
report += '建议:检查机构内部ID是否正确';
} else if (institution.ownerId !== userId) {
result.className = 'result error';
report += `❌ 权限不匹配\n`;
report += `机构负责人: ${institution.ownerId}\n`;
report += `当前用户: ${userId}\n`;
report += '建议:检查机构归属关系';
} else {
result.className = 'result success';
report += '✅ 权限验证通过\n';
report += `用户 ${user.name} 有权限向机构 ${institution.name} 上传图片`;
}
result.textContent = report;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 权限检查失败:${error.message}`;
}
}
// 显示数据统计
function showDataStats() {
const result = document.getElementById('statsResult');
result.style.display = 'block';
result.className = 'result success';
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const normalUsers = users.filter(u => u.role === 'user');
const admins = users.filter(u => u.role === 'admin');
let totalImages = 0;
institutions.forEach(inst => {
if (inst.images) {
totalImages += inst.images.length;
}
});
const report = `📊 系统数据统计
👥 用户统计:
- 总用户数:${users.length}
- 普通用户:${normalUsers.length}
- 管理员:${admins.length}
🏢 机构统计:
- 总机构数:${institutions.length}
- 有负责人的机构:${institutions.filter(i => i.ownerId).length}
- 无负责人的机构:${institutions.filter(i => !i.ownerId).length}
📸 图片统计:
- 总图片数:${totalImages}
- 平均每机构:${institutions.length > 0 ? (totalImages / institutions.length).toFixed(1) : 0}
💾 存储信息:
- 数据大小:${(JSON.stringify({institutions, users}).length / 1024).toFixed(1)} KB
- 最后更新:${new Date().toLocaleString()}`;
result.textContent = report;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 统计失败:${error.message}`;
}
}
// 列出所有机构
function listAllInstitutions() {
const result = document.getElementById('statsResult');
result.style.display = 'block';
result.className = 'result success';
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
let report = '🏢 所有机构列表\n\n';
institutions.forEach((inst, index) => {
const owner = users.find(u => u.id === inst.ownerId);
report += `${index + 1}. ${inst.name}\n`;
report += ` 编号: ${inst.institutionId}\n`;
report += ` 内部ID: ${inst.id}\n`;
report += ` 负责人: ${owner ? owner.name : '无'} (${inst.ownerId || '无'})\n`;
report += ` 图片: ${inst.images ? inst.images.length : 0} 张\n\n`;
});
result.textContent = report;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 列出机构失败:${error.message}`;
}
}
// 列出所有用户
function listAllUsers() {
const result = document.getElementById('statsResult');
result.style.display = 'block';
result.className = 'result success';
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
let report = '👥 所有用户列表\n\n';
users.forEach((user, index) => {
const userInsts = institutions.filter(i => i.ownerId === user.id);
report += `${index + 1}. ${user.name} (${user.role})\n`;
report += ` ID: ${user.id}\n`;
report += ` 手机: ${user.phone}\n`;
report += ` 负责机构: ${userInsts.length} 个\n`;
if (userInsts.length > 0) {
userInsts.forEach(inst => {
report += ` - ${inst.name}\n`;
});
}
report += '\n';
});
result.textContent = report;
} catch (error) {
result.className = 'result error';
result.textContent = `❌ 列出用户失败:${error.message}`;
}
}
// 显示修复指南
function showFixInstructions() {
const result = document.getElementById('fixResult');
result.style.display = 'block';
result.className = 'result warning';
const instructions = `🚀 修复指南
如果诊断发现问题,请按以下步骤修复:
1️⃣ 登录管理员账户
- 访问:http://localhost:5174/
- 用户名:admin
- 密码:admin123
2️⃣ 进入数据管理
- 点击顶部"数据管理"标签页
- 找到"数据修复"卡片
3️⃣ 执行修复操作
- 修复图片归属:解决图片显示错误问题
- 修复权限验证:解决权限验证错误
- 修复ID问题:解决虚拟/真实机构ID问题
- 测试权限:验证修复结果
4️⃣ 常见问题修复顺序
1. 先执行"修复ID问题"
2. 再执行"修复权限验证"
3. 最后执行"修复图片归属"
4. 用"测试权限"验证结果
5️⃣ 验证修复结果
- 重新运行此诊断页面
- 尝试上传图片功能
- 检查数据是否正常
⚠️ 注意事项:
- 修复操作会自动备份数据
- 建议在修复前导出数据备份
- 如果问题持续,请联系技术支持`;
result.textContent = instructions;
}
// 页面加载时自动检查系统状态
window.onload = function() {
checkSystemStatus();
};
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统状态检查</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
border: 1px solid #ddd;
}
.status-ok {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.status-error {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.status-checking {
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.btn {
padding: 10px 20px;
margin: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #007bff;
color: white;
}
.btn:hover {
background-color: #0056b3;
}
.info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>绩效计分系统 - 状态检查</h1>
<div id="frontendStatus" class="status-item status-checking">
<span>前端服务器 (http://localhost:5173)</span>
<span>检查中...</span>
</div>
<div id="websocketStatus" class="status-item status-checking">
<span>WebSocket服务器 (http://localhost:3000)</span>
<span>检查中...</span>
</div>
<div id="dataStatus" class="status-item status-checking">
<span>本地数据存储</span>
<span>检查中...</span>
</div>
<div id="syncStatus" class="status-item status-checking">
<span>数据同步功能</span>
<span>检查中...</span>
</div>
<div class="info">
<h3>系统信息</h3>
<p><strong>用户数量:</strong> <span id="userCount">-</span></p>
<p><strong>机构数量:</strong> <span id="institutionCount">-</span></p>
<p><strong>浏览器:</strong> <span id="browserInfo">-</span></p>
<p><strong>检查时间:</strong> <span id="checkTime">-</span></p>
</div>
<div>
<button class="btn" onclick="checkStatus()">重新检查</button>
<button class="btn" onclick="window.open('/', '_blank')">打开主应用</button>
<button class="btn" onclick="window.open('/sync-test.html', '_blank')">打开同步测试</button>
</div>
</div>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script>
function updateStatus(elementId, status, message) {
const element = document.getElementById(elementId);
element.className = `status-item status-${status}`;
element.querySelector('span:last-child').textContent = message;
}
function updateInfo() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
document.getElementById('userCount').textContent = users.length;
document.getElementById('institutionCount').textContent = institutions.length;
document.getElementById('browserInfo').textContent = navigator.userAgent.split(' ')[0];
document.getElementById('checkTime').textContent = new Date().toLocaleString();
} catch (error) {
console.error('更新信息失败:', error);
}
}
async function checkFrontendServer() {
try {
const response = await fetch('/');
if (response.ok) {
updateStatus('frontendStatus', 'ok', '运行正常');
return true;
} else {
updateStatus('frontendStatus', 'error', `HTTP ${response.status}`);
return false;
}
} catch (error) {
updateStatus('frontendStatus', 'error', '连接失败');
return false;
}
}
async function checkWebSocketServer() {
try {
const response = await fetch('http://localhost:3000');
if (response.ok) {
updateStatus('websocketStatus', 'ok', '运行正常');
return true;
} else {
updateStatus('websocketStatus', 'error', `HTTP ${response.status}`);
return false;
}
} catch (error) {
updateStatus('websocketStatus', 'error', '连接失败');
return false;
}
}
function checkLocalData() {
try {
const users = localStorage.getItem('score_system_users');
const institutions = localStorage.getItem('score_system_institutions');
if (users !== null && institutions !== null) {
updateStatus('dataStatus', 'ok', '数据存在');
return true;
} else {
updateStatus('dataStatus', 'error', '数据缺失');
return false;
}
} catch (error) {
updateStatus('dataStatus', 'error', '读取失败');
return false;
}
}
function checkDataSync() {
return new Promise((resolve) => {
try {
const socket = io('http://localhost:3000', {
timeout: 5000
});
const timeout = setTimeout(() => {
socket.disconnect();
updateStatus('syncStatus', 'error', '连接超时');
resolve(false);
}, 5000);
socket.on('connect', () => {
clearTimeout(timeout);
socket.disconnect();
updateStatus('syncStatus', 'ok', '连接正常');
resolve(true);
});
socket.on('connect_error', () => {
clearTimeout(timeout);
updateStatus('syncStatus', 'error', '连接失败');
resolve(false);
});
} catch (error) {
updateStatus('syncStatus', 'error', '检查失败');
resolve(false);
}
});
}
async function checkStatus() {
console.log('开始状态检查...');
// 重置状态
updateStatus('frontendStatus', 'checking', '检查中...');
updateStatus('websocketStatus', 'checking', '检查中...');
updateStatus('dataStatus', 'checking', '检查中...');
updateStatus('syncStatus', 'checking', '检查中...');
// 检查各个组件
const frontendOk = await checkFrontendServer();
const websocketOk = await checkWebSocketServer();
const dataOk = checkLocalData();
const syncOk = await checkDataSync();
// 更新信息
updateInfo();
console.log('状态检查完成:', {
frontend: frontendOk,
websocket: websocketOk,
data: dataOk,
sync: syncOk
});
}
// 页面加载时自动检查
document.addEventListener('DOMContentLoaded', () => {
setTimeout(checkStatus, 1000);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据同步测试</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-weight: bold;
}
.status.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.data-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.data-section h3 {
margin-top: 0;
color: #333;
}
.data-item {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.data-item:last-child {
border-bottom: none;
}
.buttons {
margin: 20px 0;
}
.btn {
padding: 10px 20px;
margin: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin: 2px 0;
padding: 2px 0;
}
.log-entry.info {
color: #0066cc;
}
.log-entry.success {
color: #28a745;
}
.log-entry.warning {
color: #ffc107;
}
.log-entry.error {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<h1>数据同步测试页面</h1>
<div id="connectionStatus" class="status disconnected">
WebSocket: 未连接
</div>
<div class="buttons">
<button class="btn btn-primary" onclick="connectWebSocket()">连接WebSocket</button>
<button class="btn btn-warning" onclick="disconnectWebSocket()">断开连接</button>
<button class="btn btn-success" onclick="refreshData()">刷新数据</button>
<button class="btn btn-danger" onclick="clearLog()">清空日志</button>
</div>
<div class="data-section">
<h3>当前数据状态</h3>
<div id="dataDisplay">
<div class="data-item">用户数量: <span id="userCount">0</span></div>
<div class="data-item">机构数量: <span id="institutionCount">0</span></div>
<div class="data-item">在线用户: <span id="onlineCount">0</span></div>
<div class="data-item">最后更新: <span id="lastUpdate">从未</span></div>
</div>
</div>
<div class="data-section">
<h3>操作日志</h3>
<div id="log" class="log"></div>
</div>
</div>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script>
let socket = null;
let isConnected = false;
function log(message, type = 'info') {
const logElement = document.getElementById('log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logElement.appendChild(entry);
logElement.scrollTop = logElement.scrollHeight;
}
function updateConnectionStatus(connected) {
isConnected = connected;
const statusElement = document.getElementById('connectionStatus');
if (connected) {
statusElement.textContent = 'WebSocket: 已连接';
statusElement.className = 'status connected';
} else {
statusElement.textContent = 'WebSocket: 未连接';
statusElement.className = 'status disconnected';
}
}
function updateDataDisplay() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
document.getElementById('userCount').textContent = users.length;
document.getElementById('institutionCount').textContent = institutions.length;
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
log(`数据更新: ${users.length}个用户, ${institutions.length}个机构`, 'success');
} catch (error) {
log(`数据读取失败: ${error.message}`, 'error');
}
}
function connectWebSocket() {
if (socket && isConnected) {
log('WebSocket已经连接', 'warning');
return;
}
try {
socket = io('http://localhost:3000');
socket.on('connect', () => {
updateConnectionStatus(true);
log('WebSocket连接成功', 'success');
// 登录到同步系统
socket.emit('user_login', {
id: 'test_user_' + Date.now(),
name: '测试用户',
role: 'user',
department: '测试部门',
browserInfo: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
}
});
});
socket.on('disconnect', () => {
updateConnectionStatus(false);
log('WebSocket连接断开', 'warning');
});
socket.on('connect_error', (error) => {
log(`连接错误: ${error.message}`, 'error');
});
socket.on('data_changed', (data) => {
log(`收到数据变更: ${data.change?.type || '未知类型'}`, 'info');
if (data.change && ['user_added', 'institution_added', 'data_saved'].includes(data.change.type)) {
setTimeout(updateDataDisplay, 100);
}
});
socket.on('online_users', (users) => {
document.getElementById('onlineCount').textContent = users.length;
log(`在线用户更新: ${users.length}人`, 'info');
});
socket.on('login_success', (data) => {
log(`登录成功: ${data.userInfo.name}`, 'success');
});
} catch (error) {
log(`连接失败: ${error.message}`, 'error');
}
}
function disconnectWebSocket() {
if (socket) {
socket.disconnect();
socket = null;
updateConnectionStatus(false);
log('手动断开WebSocket连接', 'warning');
}
}
function refreshData() {
updateDataDisplay();
log('手动刷新数据', 'info');
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
// 监听localStorage变化
window.addEventListener('storage', (event) => {
if (event.key && event.key.startsWith('score_system_')) {
log(`检测到localStorage变化: ${event.key}`, 'info');
updateDataDisplay();
}
});
// 监听自定义数据变更事件
window.addEventListener('dataChange', (event) => {
log(`收到数据变更事件: ${event.detail.type}`, 'info');
updateDataDisplay();
});
// 监听WebSocket数据变更事件
window.addEventListener('websocket_data_change', (event) => {
log(`收到WebSocket数据变更: ${event.detail.type}`, 'info');
updateDataDisplay();
});
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => {
log('页面加载完成', 'info');
updateDataDisplay();
// 自动连接WebSocket
setTimeout(connectWebSocket, 1000);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据同步测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-weight: bold;
}
.status.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.data-display {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
border-left: 4px solid #007bff;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
padding: 10px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>数据同步测试页面</h1>
<div id="connectionStatus" class="status disconnected">
WebSocket: 未连接
</div>
<div class="data-display">
<h3>当前数据统计</h3>
<p>用户数量: <span id="userCount">0</span></p>
<p>机构数量: <span id="institutionCount">0</span></p>
<p>最后更新: <span id="lastUpdate">从未</span></p>
</div>
<div>
<button onclick="refreshData()">刷新数据</button>
<button onclick="testAddUser()">测试添加用户</button>
<button onclick="testAddInstitution()">测试添加机构</button>
<button onclick="clearLog()">清空日志</button>
</div>
<div>
<h3>同步日志</h3>
<div id="log" class="log"></div>
</div>
</div>
<script>
let socket = null;
function log(message) {
const logDiv = document.getElementById('log');
const timestamp = new Date().toLocaleTimeString();
logDiv.innerHTML += `[${timestamp}] ${message}\n`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function updateConnectionStatus(connected) {
const statusDiv = document.getElementById('connectionStatus');
if (connected) {
statusDiv.className = 'status connected';
statusDiv.textContent = 'WebSocket: 已连接';
} else {
statusDiv.className = 'status disconnected';
statusDiv.textContent = 'WebSocket: 未连接';
}
}
function updateDataDisplay() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
document.getElementById('userCount').textContent = users.length;
document.getElementById('institutionCount').textContent = institutions.length;
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
log(`数据更新: ${users.length} 用户, ${institutions.length} 机构`);
} catch (error) {
log(`更新数据显示失败: ${error.message}`);
}
}
function connectWebSocket() {
try {
socket = io('http://localhost:3000');
socket.on('connect', () => {
updateConnectionStatus(true);
log('WebSocket连接成功');
// 登录到同步系统
socket.emit('user_login', {
id: 'test_user_' + Date.now(),
name: '测试用户',
role: 'user',
department: '测试部门',
browserInfo: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
}
});
});
socket.on('disconnect', () => {
updateConnectionStatus(false);
log('WebSocket连接断开');
});
socket.on('data_changed', (data) => {
log(`收到数据变更: ${data.change?.type || '未知类型'}`);
if (data.change && (data.change.type === 'user_added' || data.change.type === 'institution_added' || data.change.type === 'data_saved')) {
setTimeout(updateDataDisplay, 100); // 稍微延迟以确保localStorage已更新
}
});
socket.on('login_success', (data) => {
log(`登录成功: ${data.userInfo?.name}, 在线用户: ${data.onlineUserCount}`);
});
} catch (error) {
log(`WebSocket连接失败: ${error.message}`);
}
}
function refreshData() {
updateDataDisplay();
log('手动刷新数据');
}
function testAddUser() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const newUser = {
id: 'test_user_' + Date.now(),
name: '测试用户' + (users.length + 1),
phone: '1380000' + String(users.length + 1).padStart(4, '0'),
password: '123456',
role: 'user',
institutions: []
};
users.push(newUser);
localStorage.setItem('score_system_users', JSON.stringify(users));
// 触发数据变更事件
if (socket && socket.connected) {
socket.emit('data_update', {
type: 'user_added',
data: { user: newUser, totalUsers: users.length },
timestamp: new Date()
});
}
updateDataDisplay();
log(`测试添加用户: ${newUser.name}`);
} catch (error) {
log(`添加用户失败: ${error.message}`);
}
}
function testAddInstitution() {
try {
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
const newInstitution = {
id: 'test_inst_' + Date.now(),
institutionId: String(institutions.length + 1).padStart(3, '0'),
name: '测试机构' + (institutions.length + 1),
ownerId: null,
images: []
};
institutions.push(newInstitution);
localStorage.setItem('score_system_institutions', JSON.stringify(institutions));
// 触发数据变更事件
if (socket && socket.connected) {
socket.emit('data_update', {
type: 'institution_added',
data: { institution: newInstitution, totalInstitutions: institutions.length },
timestamp: new Date()
});
}
updateDataDisplay();
log(`测试添加机构: ${newInstitution.name}`);
} catch (error) {
log(`添加机构失败: ${error.message}`);
}
}
function clearLog() {
document.getElementById('log').innerHTML = '';
}
// 监听localStorage变化
window.addEventListener('storage', (event) => {
if (event.key && event.key.startsWith('score_system_')) {
log(`检测到localStorage变化: ${event.key}`);
updateDataDisplay();
}
});
// 监听自定义数据变更事件
window.addEventListener('dataChange', (event) => {
log(`收到数据变更事件: ${event.detail.type}`);
updateDataDisplay();
});
// 页面加载时初始化
window.addEventListener('load', () => {
log('测试页面已加载');
updateDataDisplay();
// 加载Socket.IO客户端
const script = document.createElement('script');
script.src = 'https://cdn.socket.io/4.7.2/socket.io.min.js';
script.onload = () => {
log('Socket.IO客户端已加载');
connectWebSocket();
};
script.onerror = () => {
log('Socket.IO客户端加载失败');
};
document.head.appendChild(script);
});
</script>
</body>
</html>
......@@ -395,9 +395,112 @@ export const useDataStore = defineStore('data', () => {
}
return false
}
/**
* 计算图片内容哈希值(用于重复检测)
*/
const calculateImageHash = (imageUrl) => {
// 简单的哈希算法,基于图片数据的前1000个字符
const data = imageUrl.substring(0, 1000)
let hash = 0
for (let i = 0; i < data.length; i++) {
const char = data.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // 转换为32位整数
}
return Math.abs(hash).toString(36)
}
/**
* 检测重复图片
*/
const detectDuplicateImage = (imageData, currentUserId = null) => {
console.log('🔍 开始检测重复图片...')
const result = {
isDuplicate: false,
duplicateType: null,
duplicateInfo: null,
allowUpload: true,
message: ''
}
// 计算当前图片的哈希值
const currentHash = calculateImageHash(imageData.url)
const currentSize = imageData.size
const currentName = imageData.name
console.log('当前图片信息:', {
name: currentName,
size: currentSize,
hash: currentHash
})
// 遍历所有机构的所有图片进行比较
for (const institution of institutions.value) {
if (!institution.images || institution.images.length === 0) continue
for (const existingImage of institution.images) {
const existingHash = calculateImageHash(existingImage.url)
const existingSize = existingImage.size
const existingName = existingImage.name
// 1. 检查完全相同的图片(内容和大小都相同)
if (currentHash === existingHash && currentSize === existingSize) {
result.isDuplicate = true
result.duplicateType = 'identical'
result.duplicateInfo = {
institutionName: institution.name,
imageName: existingName,
uploadTime: existingImage.uploadTime
}
result.allowUpload = false
result.message = `检测到完全相同的图片已存在于机构"${institution.name}"中`
console.log('❌ 发现完全相同的图片:', result.duplicateInfo)
return result
}
// 2. 检查内容相似但大小不同的图片(可能是轻微编辑)
if (currentHash === existingHash && Math.abs(currentSize - existingSize) < existingSize * 0.1) {
// 大小差异小于10%,认为是轻微编辑
result.isDuplicate = true
result.duplicateType = 'similar_edit'
result.duplicateInfo = {
institutionName: institution.name,
imageName: existingName,
uploadTime: existingImage.uploadTime,
sizeDiff: Math.abs(currentSize - existingSize)
}
result.allowUpload = true // 允许轻微编辑后的图片上传
result.message = `检测到相似图片(可能是轻微编辑),允许上传`
console.log('⚠️ 发现相似图片(轻微编辑):', result.duplicateInfo)
return result
}
// 3. 检查文件名相同但内容不同的图片
if (currentName === existingName && currentHash !== existingHash) {
result.isDuplicate = true
result.duplicateType = 'same_name_different_content'
result.duplicateInfo = {
institutionName: institution.name,
imageName: existingName,
uploadTime: existingImage.uploadTime
}
result.allowUpload = true // 允许文件名相同但内容不同的图片
result.message = `检测到同名但内容不同的图片,允许上传`
console.log('ℹ️ 发现同名不同内容图片:', result.duplicateInfo)
return result
}
}
}
console.log('✅ 未发现重复图片,允许上传')
result.message = '未发现重复图片'
return result
}
/**
* 为机构添加图片(带多重权限验证)
* 为机构添加图片(带多重权限验证和重复检测
*/
const addImageToInstitution = (institutionId, imageData, currentUserId = null) => {
console.log('=== addImageToInstitution 开始 ===')
......@@ -414,6 +517,20 @@ export const useDataStore = defineStore('data', () => {
throw new Error('安全验证失败:用户身份验证失败')
}
// 🔍 重复图片检测
console.log('🔍 开始重复图片检测...')
const duplicateCheck = detectDuplicateImage(imageData, currentUserId)
if (duplicateCheck.isDuplicate && !duplicateCheck.allowUpload) {
console.error('❌ 重复图片检测失败:', duplicateCheck.message)
throw new Error(duplicateCheck.message)
}
if (duplicateCheck.isDuplicate && duplicateCheck.allowUpload) {
console.warn('⚠️ 重复图片检测警告:', duplicateCheck.message)
// 继续上传,但记录警告信息
}
// 🔒 第二重验证:检查用户是否存在
const currentUser = users.value.find(user => user.id === currentUserId)
if (!currentUser) {
......@@ -486,7 +603,13 @@ export const useDataStore = defineStore('data', () => {
const newImage = {
id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
...imageData,
uploadTime: new Date().toISOString()
uploadTime: new Date().toISOString(),
hash: calculateImageHash(imageData.url),
duplicateCheck: duplicateCheck.isDuplicate ? {
type: duplicateCheck.duplicateType,
info: duplicateCheck.duplicateInfo,
message: duplicateCheck.message
} : null
}
console.log('创建新图片对象:', newImage)
......@@ -904,6 +1027,619 @@ export const useDataStore = defineStore('data', () => {
}
/**
* 修复图片归属错误问题
*/
const fixImageOwnershipIssues = () => {
console.log('🔧 开始修复图片归属错误问题...')
let fixedCount = 0
const fixedIssues = []
// 检查特定的问题机构
const problemInstitutions = [
{ keywords: ['五华区长青口腔诊所', '长青口腔'], expectedOwner: '陈锐屏' },
{ keywords: ['昆明市五华区爱雅仕口腔诊所', '爱雅仕口腔'], expectedOwner: '陈锐屏' },
{ keywords: ['昆明美云口腔医院有限公司安宁口腔诊所', '美云口腔', '安宁口腔'], expectedOwner: null },
{ keywords: ['兰州至善振林康美口腔医疗有限责任公司', '至善振林', '康美口腔'], expectedOwner: null }
]
problemInstitutions.forEach(problem => {
const matchingInstitutions = institutions.value.filter(inst =>
problem.keywords.some(keyword => inst.name.includes(keyword))
)
if (matchingInstitutions.length > 0) {
console.log(`发现问题机构组: ${problem.keywords[0]}`)
matchingInstitutions.forEach(inst => {
console.log(` - ${inst.name} (ID: ${inst.institutionId}, 负责人: ${inst.ownerId})`)
if (problem.expectedOwner) {
const expectedUser = users.value.find(user => user.name === problem.expectedOwner)
if (expectedUser && inst.ownerId !== expectedUser.id) {
console.log(` 🔧 修复归属: ${inst.ownerId} -> ${expectedUser.id}`)
inst.ownerId = expectedUser.id
fixedIssues.push(`修复机构 ${inst.name} 归属到 ${expectedUser.name}`)
fixedCount++
}
}
})
// 检查是否有图片归属错误
if (matchingInstitutions.length > 1) {
console.log(` ⚠️ 发现多个相似机构,可能存在图片归属混乱`)
// 合并相似机构的图片到正确的机构
const primaryInstitution = matchingInstitutions[0]
const secondaryInstitutions = matchingInstitutions.slice(1)
secondaryInstitutions.forEach(secondary => {
if (secondary.images && secondary.images.length > 0) {
console.log(` 🔧 移动 ${secondary.images.length} 张图片从 ${secondary.name}${primaryInstitution.name}`)
if (!primaryInstitution.images) primaryInstitution.images = []
primaryInstitution.images.push(...secondary.images)
secondary.images = []
fixedIssues.push(`移动图片从 ${secondary.name}${primaryInstitution.name}`)
fixedCount++
}
})
}
}
})
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🔧 图片归属修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现图片归属问题')
}
return {
fixed: fixedCount,
issues: fixedIssues
}
}
/**
* 修复机构权限验证错误
*/
const fixInstitutionPermissionErrors = () => {
console.log('🔒 开始修复机构权限验证错误...')
let fixedCount = 0
const fixedIssues = []
// 1. 检查机构ID重复问题
console.log('1️⃣ 检查机构ID重复问题...')
const institutionIdMap = new Map()
const internalIdMap = new Map()
institutions.value.forEach(institution => {
// 检查机构ID重复
if (institution.institutionId) {
if (institutionIdMap.has(institution.institutionId)) {
const existing = institutionIdMap.get(institution.institutionId)
console.error(`🚨 发现重复机构ID: ${institution.institutionId}`)
console.error(` - 机构1: ${existing.name} (内部ID: ${existing.id}, 负责人: ${existing.ownerId})`)
console.error(` - 机构2: ${institution.name} (内部ID: ${institution.id}, 负责人: ${institution.ownerId})`)
// 为重复的机构生成新的ID
const newId = generateNextInstitutionId()
console.log(` 🔧 为机构 ${institution.name} 分配新ID: ${newId}`)
institution.institutionId = newId
fixedIssues.push(`修复重复机构ID: ${institution.name} -> ${newId}`)
fixedCount++
} else {
institutionIdMap.set(institution.institutionId, institution)
}
}
// 检查内部ID重复
if (institution.id) {
if (internalIdMap.has(institution.id)) {
const existing = internalIdMap.get(institution.id)
console.error(`🚨 发现重复内部ID: ${institution.id}`)
console.error(` - 机构1: ${existing.name}`)
console.error(` - 机构2: ${institution.name}`)
// 为重复的机构生成新的内部ID
const newInternalId = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(` 🔧 为机构 ${institution.name} 分配新内部ID: ${newInternalId}`)
institution.id = newInternalId
fixedIssues.push(`修复重复内部ID: ${institution.name} -> ${newInternalId}`)
fixedCount++
} else {
internalIdMap.set(institution.id, institution)
}
}
})
// 2. 检查特定的问题机构
console.log('2️⃣ 检查特定问题机构...')
const problemCases = [
{
userKeywords: ['昆明美云口腔医院', '安宁口腔'],
institutionKeywords: ['昆明美云口腔医院有限公司安宁口腔诊所', '美云口腔', '安宁口腔'],
conflictKeywords: ['兰州至善振林康美口腔医疗有限责任公司', '至善振林', '康美口腔'],
description: '昆明美云口腔医院安宁口腔诊所权限错误'
},
{
userKeywords: ['五华区长青口腔', '长青口腔'],
institutionKeywords: ['五华区长青口腔诊所', '长青口腔'],
conflictKeywords: ['昆明市五华区爱雅仕口腔诊所', '爱雅仕口腔'],
description: '五华区长青口腔诊所权限错误'
}
]
problemCases.forEach(problemCase => {
console.log(`检查问题: ${problemCase.description}`)
// 查找相关机构
const targetInstitutions = institutions.value.filter(inst =>
problemCase.institutionKeywords.some(keyword => inst.name.includes(keyword))
)
const conflictInstitutions = institutions.value.filter(inst =>
problemCase.conflictKeywords.some(keyword => inst.name.includes(keyword))
)
if (targetInstitutions.length > 0 && conflictInstitutions.length > 0) {
console.log(` 发现问题机构组:`)
console.log(` 目标机构: ${targetInstitutions.map(i => i.name).join(', ')}`)
console.log(` 冲突机构: ${conflictInstitutions.map(i => i.name).join(', ')}`)
// 检查是否有权限交叉
targetInstitutions.forEach(target => {
conflictInstitutions.forEach(conflict => {
if (target.ownerId === conflict.ownerId && target.ownerId) {
console.warn(` ⚠️ 发现权限交叉: 用户 ${target.ownerId} 同时负责 ${target.name}${conflict.name}`)
fixedIssues.push(`发现权限交叉: ${target.name}${conflict.name}`)
}
})
})
}
})
// 3. 检查用户权限映射
console.log('3️⃣ 检查用户权限映射...')
users.value.forEach(user => {
if (user.role === 'user') {
const userInstitutions = institutions.value.filter(inst => inst.ownerId === user.id)
if (userInstitutions.length > 0) {
console.log(`用户 ${user.name} (${user.id}) 负责 ${userInstitutions.length} 个机构:`)
userInstitutions.forEach(inst => {
console.log(` - ${inst.name} (机构ID: ${inst.institutionId}, 内部ID: ${inst.id})`)
})
// 检查是否有跨地区的机构归属(可能的权限错误)
const regions = new Set()
userInstitutions.forEach(inst => {
if (inst.name.includes('五华区') || inst.name.includes('昆明')) {
regions.add('昆明')
} else if (inst.name.includes('大连')) {
regions.add('大连')
} else if (inst.name.includes('兰州')) {
regions.add('兰州')
} else if (inst.name.includes('安宁')) {
regions.add('安宁')
} else if (inst.name.includes('北京')) {
regions.add('北京')
} else if (inst.name.includes('上海')) {
regions.add('上海')
}
})
if (regions.size > 1) {
console.warn(`⚠️ 用户 ${user.name} 负责跨地区机构: ${Array.from(regions).join(', ')}`)
fixedIssues.push(`用户 ${user.name} 负责跨地区机构: ${Array.from(regions).join(', ')}`)
}
// 检查是否有机构名称相似但归属不同的情况
const institutionNames = userInstitutions.map(inst => inst.name)
institutionNames.forEach(name => {
const similarInstitutions = institutions.value.filter(inst =>
inst.name.includes(name.split('口腔')[0]) && inst.ownerId !== user.id
)
if (similarInstitutions.length > 0) {
console.warn(`⚠️ 发现相似机构名称但归属不同:`)
console.warn(` 用户机构: ${name}`)
similarInstitutions.forEach(similar => {
console.warn(` 相似机构: ${similar.name} (归属: ${similar.ownerId})`)
})
fixedIssues.push(`发现相似机构名称但归属不同: ${name}`)
}
})
}
}
})
// 4. 验证localStorage数据一致性
console.log('4️⃣ 验证localStorage数据一致性...')
try {
const savedData = localStorage.getItem('score_system_institutions')
if (savedData) {
const savedInstitutions = JSON.parse(savedData)
// 检查内存数据和localStorage数据是否一致
if (savedInstitutions.length !== institutions.value.length) {
console.warn(`⚠️ 数据不一致: 内存中 ${institutions.value.length} 个机构,localStorage中 ${savedInstitutions.length} 个机构`)
fixedIssues.push(`数据不一致: 内存和localStorage机构数量不匹配`)
}
// 检查具体的机构数据
institutions.value.forEach(memInst => {
const savedInst = savedInstitutions.find(si => si.id === memInst.id)
if (!savedInst) {
console.warn(`⚠️ 机构 ${memInst.name} 在localStorage中不存在`)
fixedIssues.push(`机构 ${memInst.name} 在localStorage中不存在`)
} else if (savedInst.ownerId !== memInst.ownerId) {
console.warn(`⚠️ 机构 ${memInst.name} 的负责人不一致: 内存(${memInst.ownerId}) vs localStorage(${savedInst.ownerId})`)
fixedIssues.push(`机构 ${memInst.name} 负责人数据不一致`)
}
})
}
} catch (error) {
console.error('检查localStorage数据时出错:', error)
fixedIssues.push('localStorage数据检查失败')
}
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🔒 权限验证修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现需要修复的权限验证问题')
}
return {
fixed: fixedCount,
issues: fixedIssues
}
}
/**
* 测试图片上传权限验证
*/
const testImageUploadPermissions = (userId) => {
console.log('🧪 开始测试图片上传权限验证...')
const user = users.value.find(u => u.id === userId)
if (!user) {
console.error('❌ 用户不存在:', userId)
return { success: false, error: '用户不存在' }
}
console.log(`测试用户: ${user.name} (${user.id})`)
const userInstitutions = institutions.value.filter(inst => inst.ownerId === userId)
console.log(`用户负责的机构数量: ${userInstitutions.length}`)
const testResults = []
userInstitutions.forEach(institution => {
console.log(`\n测试机构: ${institution.name}`)
console.log(` - 机构ID: ${institution.institutionId}`)
console.log(` - 内部ID: ${institution.id}`)
console.log(` - 负责人: ${institution.ownerId}`)
try {
// 模拟图片数据
const mockImageData = {
id: `test_${Date.now()}`,
name: `测试图片_${institution.name}.jpg`,
url: 'data:image/jpeg;base64,test',
size: 1024,
uploadTime: new Date().toISOString()
}
// 测试权限验证逻辑(不实际添加图片)
console.log(' 🔒 开始权限验证测试...')
// 第一重验证:检查用户ID
if (!userId) {
throw new Error('用户ID验证失败')
}
// 第二重验证:检查用户是否存在
const currentUser = users.value.find(user => user.id === userId)
if (!currentUser) {
throw new Error('用户不存在')
}
// 第三重验证:检查机构是否存在
const inst = institutions.value.find(inst => inst.id === institution.id)
if (!inst) {
throw new Error('机构不存在')
}
// 第四重验证:检查权限
if (inst.ownerId !== userId) {
throw new Error(`权限验证失败: 机构负责人(${inst.ownerId}) != 当前用户(${userId})`)
}
// 第五重验证:双重确认机构归属
const userInsts = institutions.value.filter(inst => inst.ownerId === userId)
const isUserInst = userInsts.some(inst => inst.id === institution.id)
if (!isUserInst) {
throw new Error('双重验证失败: 机构不在用户机构列表中')
}
console.log(' ✅ 权限验证测试通过')
testResults.push({
institutionId: institution.id,
institutionName: institution.name,
success: true,
message: '权限验证通过'
})
} catch (error) {
console.error(` ❌ 权限验证测试失败: ${error.message}`)
testResults.push({
institutionId: institution.id,
institutionName: institution.name,
success: false,
error: error.message
})
}
})
// 测试其他用户的机构(应该失败)
console.log('\n🧪 测试访问其他用户机构(应该失败)...')
const otherInstitutions = institutions.value.filter(inst => inst.ownerId !== userId).slice(0, 3)
otherInstitutions.forEach(institution => {
console.log(`测试无权访问的机构: ${institution.name}`)
try {
if (institution.ownerId !== userId) {
throw new Error(`权限验证失败: 无权操作机构"${institution.name}"`)
}
console.error(' ❌ 安全漏洞: 权限验证应该失败但却通过了!')
testResults.push({
institutionId: institution.id,
institutionName: institution.name,
success: false,
error: '安全漏洞: 权限验证应该失败但却通过了'
})
} catch (error) {
console.log(` ✅ 正确拒绝: ${error.message}`)
testResults.push({
institutionId: institution.id,
institutionName: institution.name,
success: true,
message: '正确拒绝无权访问'
})
}
})
const successCount = testResults.filter(r => r.success).length
const totalCount = testResults.length
console.log(`\n🧪 权限验证测试完成: ${successCount}/${totalCount} 通过`)
return {
success: successCount === totalCount,
userId: userId,
userName: user.name,
totalTests: totalCount,
passedTests: successCount,
results: testResults
}
}
/**
* 修复虚拟机构和真实机构ID问题
*/
const fixVirtualRealInstitutionIds = () => {
console.log('🔧 开始修复虚拟机构和真实机构ID问题...')
let fixedCount = 0
const fixedIssues = []
// 1. 检查并修复缺失的ID
console.log('1️⃣ 检查并修复缺失的ID...')
institutions.value.forEach(institution => {
let needsFix = false
// 确保有内部ID
if (!institution.id) {
institution.id = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(`为机构 ${institution.name} 生成内部ID: ${institution.id}`)
fixedIssues.push(`为机构 ${institution.name} 生成内部ID`)
needsFix = true
}
// 确保有机构编号
if (!institution.institutionId) {
institution.institutionId = generateNextInstitutionId()
console.log(`为机构 ${institution.name} 生成机构编号: ${institution.institutionId}`)
fixedIssues.push(`为机构 ${institution.name} 生成机构编号`)
needsFix = true
}
// 确保有负责人ID(如果没有,设为null)
if (institution.ownerId === undefined) {
institution.ownerId = null
console.log(`机构 ${institution.name} 设置为无负责人状态`)
fixedIssues.push(`机构 ${institution.name} 设置为无负责人状态`)
needsFix = true
}
// 确保有images数组
if (!institution.images) {
institution.images = []
console.log(`为机构 ${institution.name} 初始化images数组`)
fixedIssues.push(`为机构 ${institution.name} 初始化images数组`)
needsFix = true
}
if (needsFix) {
fixedCount++
}
})
// 2. 检查并修复ID格式问题
console.log('2️⃣ 检查并修复ID格式问题...')
institutions.value.forEach(institution => {
// 检查机构编号格式(应该是纯数字,如001、002)
if (institution.institutionId && !/^\d{3}$/.test(institution.institutionId)) {
const oldId = institution.institutionId
institution.institutionId = generateNextInstitutionId()
console.log(`修复机构 ${institution.name} 的编号格式: ${oldId} -> ${institution.institutionId}`)
fixedIssues.push(`修复机构 ${institution.name} 的编号格式`)
fixedCount++
}
// 检查内部ID格式(应该是inst_开头)
if (institution.id && !institution.id.startsWith('inst_')) {
const oldId = institution.id
institution.id = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(`修复机构 ${institution.name} 的内部ID格式: ${oldId} -> ${institution.id}`)
fixedIssues.push(`修复机构 ${institution.name} 的内部ID格式`)
fixedCount++
}
})
// 3. 检查并修复负责人关联
console.log('3️⃣ 检查并修复负责人关联...')
institutions.value.forEach(institution => {
if (institution.ownerId) {
const owner = users.value.find(user => user.id === institution.ownerId)
if (!owner) {
console.warn(`机构 ${institution.name} 的负责人 ${institution.ownerId} 不存在,设为无负责人状态`)
institution.ownerId = null
fixedIssues.push(`机构 ${institution.name} 的负责人不存在,已清理`)
fixedCount++
}
}
})
// 4. 验证修复结果
console.log('4️⃣ 验证修复结果...')
const validationErrors = []
institutions.value.forEach(institution => {
if (!institution.id) {
validationErrors.push(`机构 ${institution.name} 仍然缺少内部ID`)
}
if (!institution.institutionId) {
validationErrors.push(`机构 ${institution.name} 仍然缺少机构编号`)
}
if (!Array.isArray(institution.images)) {
validationErrors.push(`机构 ${institution.name} 的images不是数组`)
}
})
if (validationErrors.length > 0) {
console.error('验证失败:', validationErrors)
fixedIssues.push(...validationErrors)
} else {
console.log('✅ 验证通过,所有机构数据结构正确')
}
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🔧 虚拟/真实机构ID修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现需要修复的ID问题')
}
return {
fixed: fixedCount,
issues: fixedIssues
}
}
/**
* 诊断图片上传问题
*/
const diagnoseImageUploadIssue = (userId, institutionInternalId) => {
console.log('🔍 诊断图片上传问题...')
console.log(`用户ID: ${userId}`)
console.log(`机构内部ID: ${institutionInternalId}`)
const diagnosticResult = {
success: false,
issues: [],
suggestions: []
}
// 1. 检查用户是否存在
const user = users.value.find(u => u.id === userId)
if (!user) {
diagnosticResult.issues.push('用户不存在')
diagnosticResult.suggestions.push('检查用户ID是否正确')
return diagnosticResult
}
console.log(`找到用户: ${user.name} (${user.role})`)
// 2. 检查机构是否存在
const institution = institutions.value.find(i => i.id === institutionInternalId)
if (!institution) {
diagnosticResult.issues.push('机构不存在')
diagnosticResult.suggestions.push('检查机构内部ID是否正确')
// 尝试通过机构编号查找
const instByNumber = institutions.value.find(i => i.institutionId === institutionInternalId)
if (instByNumber) {
diagnosticResult.suggestions.push(`可能混淆了机构编号(${institutionInternalId})和内部ID(${instByNumber.id})`)
}
return diagnosticResult
}
console.log(`找到机构: ${institution.name}`)
console.log(`机构编号: ${institution.institutionId}`)
console.log(`机构负责人: ${institution.ownerId}`)
// 3. 检查权限
if (institution.ownerId !== userId) {
diagnosticResult.issues.push(`权限不匹配: 机构负责人(${institution.ownerId}) != 当前用户(${userId})`)
const actualOwner = users.value.find(u => u.id === institution.ownerId)
if (actualOwner) {
diagnosticResult.suggestions.push(`机构 ${institution.name} 的实际负责人是 ${actualOwner.name}`)
} else {
diagnosticResult.suggestions.push('机构没有有效的负责人,需要重新分配')
}
return diagnosticResult
}
// 4. 检查机构数据结构
if (!institution.images) {
diagnosticResult.issues.push('机构缺少images数组')
diagnosticResult.suggestions.push('执行数据结构修复')
return diagnosticResult
}
if (!Array.isArray(institution.images)) {
diagnosticResult.issues.push('机构的images不是数组')
diagnosticResult.suggestions.push('执行数据结构修复')
return diagnosticResult
}
// 5. 检查用户机构列表
const userInstitutions = institutions.value.filter(inst => inst.ownerId === userId)
const isInUserList = userInstitutions.some(inst => inst.id === institutionInternalId)
if (!isInUserList) {
diagnosticResult.issues.push('机构不在用户的机构列表中')
diagnosticResult.suggestions.push('检查机构归属关系')
return diagnosticResult
}
console.log('✅ 所有检查通过,应该可以正常上传图片')
diagnosticResult.success = true
return diagnosticResult
}
/**
* 全面数据完整性检查和修复
*/
const comprehensiveDataIntegrityCheck = () => {
......@@ -1023,6 +1759,152 @@ export const useDataStore = defineStore('data', () => {
}
/**
* 历史统计数据管理
*/
// 历史统计数据存储键
const HISTORY_STORAGE_KEY = 'score_system_history'
/**
* 保存当前月份的统计数据到历史记录
*/
const saveCurrentMonthStats = () => {
try {
const currentDate = new Date()
const monthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
console.log(`📊 保存 ${monthKey} 月份统计数据...`)
// 获取当前所有用户的统计数据
const currentStats = users.value
.filter(user => user.role === 'user')
.map(user => {
const userInstitutions = getInstitutionsByUserId(user.id)
return {
userId: user.id,
userName: user.name,
institutionCount: userInstitutions.length,
interactionScore: calculateInteractionScore(user.id),
performanceScore: calculatePerformanceScore(user.id),
institutions: userInstitutions.map(inst => ({
id: inst.id,
name: inst.name,
imageCount: inst.images ? inst.images.length : 0
}))
}
})
// 获取现有历史数据
const historyData = JSON.parse(localStorage.getItem(HISTORY_STORAGE_KEY) || '{}')
// 保存当前月份数据
historyData[monthKey] = {
month: monthKey,
saveTime: new Date().toISOString(),
totalUsers: currentStats.length,
totalInstitutions: institutions.value.length,
totalImages: institutions.value.reduce((total, inst) => total + (inst.images ? inst.images.length : 0), 0),
userStats: currentStats
}
// 保存到localStorage
localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(historyData))
console.log(`✅ ${monthKey} 月份统计数据保存成功`)
console.log('保存的数据:', historyData[monthKey])
return true
} catch (error) {
console.error('保存历史统计数据失败:', error)
return false
}
}
/**
* 获取历史统计数据
*/
const getHistoryStats = () => {
try {
const historyData = JSON.parse(localStorage.getItem(HISTORY_STORAGE_KEY) || '{}')
return historyData
} catch (error) {
console.error('获取历史统计数据失败:', error)
return {}
}
}
/**
* 获取可用的历史月份列表
*/
const getAvailableHistoryMonths = () => {
const historyData = getHistoryStats()
return Object.keys(historyData).sort((a, b) => b.localeCompare(a)) // 按时间倒序
}
/**
* 获取指定月份的统计数据
*/
const getMonthStats = (monthKey) => {
const historyData = getHistoryStats()
return historyData[monthKey] || null
}
/**
* 删除指定月份的历史数据
*/
const deleteMonthStats = (monthKey) => {
try {
const historyData = getHistoryStats()
if (historyData[monthKey]) {
delete historyData[monthKey]
localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(historyData))
console.log(`✅ 删除 ${monthKey} 月份数据成功`)
return true
}
return false
} catch (error) {
console.error('删除历史数据失败:', error)
return false
}
}
/**
* 清空所有历史数据
*/
const clearAllHistoryStats = () => {
try {
localStorage.removeItem(HISTORY_STORAGE_KEY)
console.log('✅ 所有历史数据已清空')
return true
} catch (error) {
console.error('清空历史数据失败:', error)
return false
}
}
/**
* 自动保存月度统计(在每月1号自动执行)
*/
const autoSaveMonthlyStats = () => {
const currentDate = new Date()
const lastSaveKey = 'last_monthly_save'
const lastSave = localStorage.getItem(lastSaveKey)
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
// 检查是否需要保存(每月只保存一次)
if (!lastSave || lastSave !== currentMonthKey) {
console.log('🔄 执行自动月度统计保存...')
const success = saveCurrentMonthStats()
if (success) {
localStorage.setItem(lastSaveKey, currentMonthKey)
}
return success
}
return false // 本月已保存过
}
/**
* 导出数据(用于备份)
*/
const exportData = () => {
......@@ -1084,6 +1966,8 @@ export const useDataStore = defineStore('data', () => {
deleteInstitution,
addImageToInstitution,
removeImageFromInstitution,
calculateImageHash,
detectDuplicateImage,
calculateInteractionScore,
calculatePerformanceScore,
getAllUserScores,
......@@ -1094,10 +1978,22 @@ export const useDataStore = defineStore('data', () => {
fixInstitutionDataStructure,
fixDataOwnership,
emergencyFixDataLeak,
fixImageOwnershipIssues,
fixInstitutionPermissionErrors,
testImageUploadPermissions,
fixVirtualRealInstitutionIds,
diagnoseImageUploadIssue,
comprehensiveDataIntegrityCheck,
clearAllData,
resetToDefault,
exportData,
importData
importData,
saveCurrentMonthStats,
getHistoryStats,
getAvailableHistoryMonths,
getMonthStats,
deleteMonthStats,
clearAllHistoryStats,
autoSaveMonthlyStats
}
})
\ No newline at end of file
/**
* 数据清理工具
* 用于清理系统中的用户和机构数据,只保留管理员
*/
import { useDataStore } from '@/store/data.js'
import { ElMessage, ElMessageBox } from 'element-plus'
/**
* 清理所有非管理员数据
*/
export const cleanupAllNonAdminData = async () => {
try {
const result = await ElMessageBox.confirm(
'此操作将删除所有用户数据和机构数据,只保留管理员账户。此操作不可恢复,确定要继续吗?',
'数据清理确认',
{
confirmButtonText: '确定清理',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
}
)
if (result === 'confirm') {
const dataStore = useDataStore()
// 清理数据
const success = dataStore.cleanupNonAdminData()
if (success) {
ElMessage.success('数据清理完成,只保留了管理员账户')
// 清理浏览器缓存
clearBrowserCache()
// 刷新页面
setTimeout(() => {
window.location.reload()
}, 1500)
return true
} else {
ElMessage.error('数据清理失败')
return false
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('数据清理过程中出错:', error)
ElMessage.error('数据清理过程中出错')
}
return false
}
}
/**
* 清理浏览器缓存
*/
export const clearBrowserCache = () => {
try {
// 清理localStorage中的相关数据
const keysToKeep = ['score_system_users', 'score_system_institutions', 'score_system_config']
const keysToRemove = []
// 找出所有相关的localStorage键
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith('score_system_') && !keysToKeep.includes(key)) {
keysToRemove.push(key)
}
}
// 删除不需要的键
keysToRemove.forEach(key => {
localStorage.removeItem(key)
})
// 清理sessionStorage
sessionStorage.clear()
console.log('浏览器缓存已清理')
} catch (error) {
console.error('清理浏览器缓存失败:', error)
}
}
/**
* 验证数据完整性
*/
export const validateDataIntegrity = () => {
try {
const dataStore = useDataStore()
const users = dataStore.getUsers()
const institutions = dataStore.getInstitutions()
const report = {
totalUsers: users.length,
adminUsers: users.filter(u => u.role === 'admin').length,
regularUsers: users.filter(u => u.role === 'user').length,
totalInstitutions: institutions.length,
orphanedInstitutions: institutions.filter(inst => {
return !users.some(user => user.institutions && user.institutions.includes(inst.institutionId))
}).length
}
console.log('数据完整性报告:', report)
return report
} catch (error) {
console.error('数据完整性验证失败:', error)
return null
}
}
/**
* 导出当前数据(用于备份)
*/
export const exportCurrentData = () => {
try {
const dataStore = useDataStore()
const exportData = dataStore.exportData()
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `score_system_backup_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success('数据导出完成')
return true
} catch (error) {
console.error('数据导出失败:', error)
ElMessage.error('数据导出失败')
return false
}
}
/**
* 重置系统到初始状态
*/
export const resetSystemToInitialState = async () => {
try {
const result = await ElMessageBox.confirm(
'此操作将重置整个系统到初始状态,删除所有数据。此操作不可恢复,确定要继续吗?',
'系统重置确认',
{
confirmButtonText: '确定重置',
cancelButtonText: '取消',
type: 'error',
dangerouslyUseHTMLString: true
}
)
if (result === 'confirm') {
const dataStore = useDataStore()
// 重置到默认状态
const success = dataStore.resetToDefault()
if (success) {
ElMessage.success('系统重置完成')
// 清理所有缓存
localStorage.clear()
sessionStorage.clear()
// 刷新页面
setTimeout(() => {
window.location.href = '/'
}, 1500)
return true
} else {
ElMessage.error('系统重置失败')
return false
}
}
} catch (error) {
if (error !== 'cancel') {
console.error('系统重置过程中出错:', error)
ElMessage.error('系统重置过程中出错')
}
return false
}
}
/**
* 服务器端数据同步管理器
* 解决跨浏览器数据同步问题
*/
import { ElMessage } from 'element-plus'
class ServerDataSync {
constructor() {
this.serverUrl = 'http://localhost:3001'
this.isEnabled = false
this.syncInterval = null
}
/**
* 启用服务器端数据同步
*/
enable() {
this.isEnabled = true
console.log('✅ 服务器端数据同步已启用')
}
/**
* 禁用服务器端数据同步
*/
disable() {
this.isEnabled = false
if (this.syncInterval) {
clearInterval(this.syncInterval)
this.syncInterval = null
}
console.log('❌ 服务器端数据同步已禁用')
}
/**
* 从服务器获取系统数据
*/
async getSystemData() {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 3000) // 3秒超时
const response = await fetch(`${this.serverUrl}/api/system-data`, {
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
console.log('📥 从服务器获取系统数据:', data)
return data
} catch (error) {
console.error('获取服务器数据失败:', error)
throw error
}
}
/**
* 向服务器保存系统数据
*/
async saveSystemData(systemData) {
if (!this.isEnabled) {
console.log('服务器同步未启用,跳过保存')
return false
}
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5秒超时
const response = await fetch(`${this.serverUrl}/api/system-data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(systemData),
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
console.log('📤 数据已保存到服务器:', result)
return true
} catch (error) {
console.error('保存数据到服务器失败:', error)
// 不显示错误消息,因为服务器可能不可用
return false
}
}
/**
* 同步localStorage数据到服务器
*/
async syncToServer() {
try {
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]')
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]')
const systemConfig = JSON.parse(localStorage.getItem('score_system_config') || '{}')
const systemData = {
users,
institutions,
systemConfig
}
await this.saveSystemData(systemData)
console.log('✅ localStorage数据已同步到服务器')
return true
} catch (error) {
console.error('同步数据到服务器失败:', error)
return false
}
}
/**
* 从服务器同步数据到localStorage
*/
async syncFromServer() {
try {
const systemData = await this.getSystemData()
if (systemData.users) {
localStorage.setItem('score_system_users', JSON.stringify(systemData.users))
}
if (systemData.institutions) {
localStorage.setItem('score_system_institutions', JSON.stringify(systemData.institutions))
}
if (systemData.systemConfig) {
localStorage.setItem('score_system_config', JSON.stringify(systemData.systemConfig))
}
console.log('✅ 服务器数据已同步到localStorage')
// 触发数据刷新事件
window.dispatchEvent(new CustomEvent('serverDataSynced', {
detail: { systemData, timestamp: new Date().toISOString() }
}))
return true
} catch (error) {
console.error('从服务器同步数据失败:', error)
return false
}
}
/**
* 检查服务器连接状态
*/
async checkServerConnection() {
try {
const response = await fetch(`${this.serverUrl}/health`, {
method: 'GET',
timeout: 5000
})
return response.ok
} catch (error) {
console.warn('服务器连接检查失败:', error)
return false
}
}
/**
* 启动定期同步
*/
startPeriodicSync(intervalMs = 30000) {
if (this.syncInterval) {
clearInterval(this.syncInterval)
}
this.syncInterval = setInterval(async () => {
if (this.isEnabled) {
const isConnected = await this.checkServerConnection()
if (isConnected) {
await this.syncFromServer()
}
}
}, intervalMs)
console.log(`🔄 定期同步已启动,间隔: ${intervalMs}ms`)
}
/**
* 停止定期同步
*/
stopPeriodicSync() {
if (this.syncInterval) {
clearInterval(this.syncInterval)
this.syncInterval = null
console.log('⏹️ 定期同步已停止')
}
}
/**
* 强制全量同步
*/
async forceSync() {
try {
console.log('🔄 开始强制全量同步...')
// 先检查服务器连接
const isConnected = await this.checkServerConnection()
if (!isConnected) {
throw new Error('服务器连接失败')
}
// 从服务器获取最新数据
await this.syncFromServer()
ElMessage.success('数据同步完成')
return true
} catch (error) {
console.error('强制同步失败:', error)
ElMessage.error('数据同步失败: ' + error.message)
return false
}
}
/**
* 获取同步状态
*/
getStatus() {
return {
isEnabled: this.isEnabled,
hasPeriodicSync: !!this.syncInterval,
serverUrl: this.serverUrl
}
}
}
// 创建全局实例
const serverDataSync = new ServerDataSync()
// 导出实例和类
export default serverDataSync
export { ServerDataSync }
/**
* 简单的跨浏览器数据同步
* 使用localStorage和定期检查实现基本的数据同步
*/
class SimpleCrossBrowserSync {
constructor() {
this.isEnabled = false
this.syncInterval = null
this.lastSyncTime = 0
this.syncKey = 'score_system_sync_timestamp'
this.dataKeys = [
'score_system_users',
'score_system_institutions',
'score_system_config'
]
}
/**
* 启用跨浏览器同步
*/
enable() {
this.isEnabled = true
this.updateSyncTimestamp()
this.startPeriodicCheck()
console.log('✅ 简单跨浏览器同步已启用')
}
/**
* 禁用跨浏览器同步
*/
disable() {
this.isEnabled = false
if (this.syncInterval) {
clearInterval(this.syncInterval)
this.syncInterval = null
}
console.log('❌ 简单跨浏览器同步已禁用')
}
/**
* 更新同步时间戳
*/
updateSyncTimestamp() {
const timestamp = Date.now()
localStorage.setItem(this.syncKey, timestamp.toString())
this.lastSyncTime = timestamp
}
/**
* 检查是否有数据更新
*/
checkForUpdates() {
if (!this.isEnabled) return false
try {
const currentTimestamp = parseInt(localStorage.getItem(this.syncKey) || '0')
if (currentTimestamp > this.lastSyncTime) {
console.log('🔄 检测到数据更新,触发同步')
this.lastSyncTime = currentTimestamp
this.triggerDataRefresh()
return true
}
return false
} catch (error) {
console.error('检查数据更新失败:', error)
return false
}
}
/**
* 触发数据刷新事件
*/
triggerDataRefresh() {
try {
// 触发自定义事件通知页面刷新数据
const event = new CustomEvent('simpleSyncDataChanged', {
detail: {
timestamp: new Date().toISOString(),
source: 'simpleCrossBrowserSync'
}
})
window.dispatchEvent(event)
console.log('✅ 已触发数据刷新事件')
} catch (error) {
console.error('触发数据刷新事件失败:', error)
}
}
/**
* 开始定期检查
*/
startPeriodicCheck() {
if (this.syncInterval) {
clearInterval(this.syncInterval)
}
// 每5秒检查一次数据更新
this.syncInterval = setInterval(() => {
this.checkForUpdates()
}, 5000)
console.log('🔄 开始定期检查数据更新 (每5秒)')
}
/**
* 通知数据已更改
*/
notifyDataChanged() {
if (!this.isEnabled) return
this.updateSyncTimestamp()
console.log('📢 数据更改通知已发送')
}
/**
* 获取同步状态
*/
getSyncStatus() {
return {
isEnabled: this.isEnabled,
lastSyncTime: this.lastSyncTime,
syncTimestamp: parseInt(localStorage.getItem(this.syncKey) || '0')
}
}
/**
* 强制同步检查
*/
forceSyncCheck() {
console.log('🔄 强制执行同步检查')
return this.checkForUpdates()
}
}
// 创建全局实例
const simpleCrossBrowserSync = new SimpleCrossBrowserSync()
export default simpleCrossBrowserSync
/**
* 数据同步初始化器
* 统一管理WebSocket连接和数据同步逻辑
*/
import dataSyncManager from './dataSyncManager.js'
import { useAuthStore } from '@/store/auth.js'
import { useDataStore } from '@/store/data.js'
class SyncInitializer {
constructor() {
this.isInitialized = false
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
}
/**
* 初始化数据同步
*/
async initialize() {
if (this.isInitialized) {
console.log('数据同步已经初始化')
return
}
try {
const authStore = useAuthStore()
const dataStore = useDataStore()
if (!authStore.isAuthenticated) {
console.log('用户未登录,跳过数据同步初始化')
return
}
// 设置全局引用
window.dataSyncManager = dataSyncManager
window.dataStore = dataStore
// 连接WebSocket服务器
dataSyncManager.connect('http://localhost:3001')
// 准备用户信息
const userInfo = {
id: authStore.currentUser.id,
name: authStore.currentUser.name,
role: authStore.currentUser.role,
department: authStore.currentUser.department || '未知部门',
browserInfo: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
url: window.location.href
}
}
// 登录到同步系统
dataSyncManager.login(userInfo)
// 设置数据变更监听
this.setupDataChangeListeners()
this.isInitialized = true
console.log('✅ 数据同步初始化完成')
} catch (error) {
console.error('数据同步初始化失败:', error)
this.scheduleReconnect()
}
}
/**
* 设置数据变更监听器
*/
setupDataChangeListeners() {
// 监听WebSocket数据变更
window.addEventListener('websocket_data_change', (event) => {
const { type, userName, timestamp } = event.detail
console.log(`收到WebSocket数据变更: ${type} (来自: ${userName})`)
// 触发数据重新加载
this.handleDataChange(type)
})
// 监听localStorage变更
window.addEventListener('storage', (event) => {
if (event.key && event.key.startsWith('score_system_')) {
console.log('检测到localStorage变化:', event.key)
this.handleDataChange('storage_change')
}
})
// 监听自定义数据变更事件
window.addEventListener('dataChange', (event) => {
const { type } = event.detail
console.log('收到自定义数据变更事件:', type)
this.handleDataChange(type)
})
}
/**
* 处理数据变更
*/
handleDataChange(type) {
try {
// 触发页面数据刷新
const refreshEvent = new CustomEvent('dataRefreshRequired', {
detail: {
type,
timestamp: new Date().toISOString()
}
})
window.dispatchEvent(refreshEvent)
// 如果是重要的数据变更,延迟再次刷新以确保数据一致性
if (['user_added', 'user_deleted', 'institution_added', 'institution_deleted'].includes(type)) {
setTimeout(() => {
window.dispatchEvent(new CustomEvent('dataRefreshRequired', {
detail: {
type: `${type}_delayed`,
timestamp: new Date().toISOString()
}
}))
}, 1000)
}
} catch (error) {
console.error('处理数据变更失败:', error)
}
}
/**
* 安排重连
*/
scheduleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)
console.log(`${delay}ms后尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
setTimeout(() => {
this.isInitialized = false
this.initialize()
}, delay)
} else {
console.error('达到最大重连次数,停止重连')
}
}
/**
* 重置连接状态
*/
reset() {
this.isInitialized = false
this.reconnectAttempts = 0
}
/**
* 手动触发数据同步
*/
triggerSync() {
if (window.dataSyncManager) {
const dataStore = useDataStore()
// 广播当前数据状态
window.dataSyncManager.broadcastChange('data_saved', {
users: dataStore.getUsers(),
institutions: dataStore.getInstitutions(),
timestamp: new Date().toISOString()
})
console.log('手动触发数据同步')
}
}
/**
* 检查连接状态
*/
getStatus() {
return {
isInitialized: this.isInitialized,
isConnected: window.dataSyncManager?.connectionStatus?.value?.isConnected || false,
reconnectAttempts: this.reconnectAttempts,
onlineUsers: window.dataSyncManager?.data?.onlineUsers?.length || 0
}
}
}
// 创建单例实例
const syncInitializer = new SyncInitializer()
// 导出初始化函数和实例
export const initializeSync = () => syncInitializer.initialize()
export const resetSync = () => syncInitializer.reset()
export const triggerSync = () => syncInitializer.triggerSync()
export const getSyncStatus = () => syncInitializer.getStatus()
export default syncInitializer
......@@ -346,6 +346,145 @@
</div>
</el-tab-pane>
<!-- 历史统计 -->
<el-tab-pane label="历史统计" name="history-stats">
<div class="tab-content">
<div class="history-stats-container">
<!-- 历史统计头部 -->
<div class="history-header">
<div class="header-left">
<h3 class="history-title">
<el-icon><TrendCharts /></el-icon>
历史统计分析
</h3>
<p class="history-subtitle">查看历史月份各用户的绩效数据</p>
</div>
<div class="header-actions">
<el-button type="primary" @click="saveCurrentMonthStats" :loading="saveStatsLoading">
<el-icon><Download /></el-icon>
保存当前月份
</el-button>
<el-button type="warning" @click="clearHistoryStats" :loading="clearHistoryLoading">
<el-icon><Delete /></el-icon>
清空历史数据
</el-button>
</div>
</div>
<!-- 月份筛选 -->
<div class="month-filter">
<el-select
v-model="selectedHistoryMonth"
placeholder="选择历史月份"
@change="loadHistoryMonth"
style="width: 200px"
>
<el-option
v-for="month in availableMonths"
:key="month"
:label="formatMonthLabel(month)"
:value="month"
/>
</el-select>
<el-button
v-if="selectedHistoryMonth"
type="danger"
size="small"
@click="deleteSelectedMonth"
style="margin-left: 10px"
>
删除此月份数据
</el-button>
</div>
<!-- 历史数据展示 -->
<div v-if="selectedMonthData" class="history-data">
<!-- 月份概览 -->
<div class="month-overview">
<el-row :gutter="20">
<el-col :span="6">
<div class="overview-card">
<div class="overview-title">总用户数</div>
<div class="overview-value">{{ selectedMonthData.totalUsers }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="overview-card">
<div class="overview-title">总机构数</div>
<div class="overview-value">{{ selectedMonthData.totalInstitutions }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="overview-card">
<div class="overview-title">总图片数</div>
<div class="overview-value">{{ selectedMonthData.totalImages }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="overview-card">
<div class="overview-title">数据保存时间</div>
<div class="overview-value small">{{ formatDateTime(selectedMonthData.saveTime) }}</div>
</div>
</el-col>
</el-row>
</div>
<!-- 用户历史数据表格 -->
<div class="history-table">
<el-table :data="selectedMonthData.userStats" stripe style="width: 100%">
<el-table-column prop="userName" label="用户姓名" width="120" />
<el-table-column prop="institutionCount" label="负责机构数" width="120" align="center" />
<el-table-column prop="interactionScore" label="互动得分" width="120" align="center">
<template #default="scope">
{{ scope.row.interactionScore.toFixed(1) }}
</template>
</el-table-column>
<el-table-column prop="performanceScore" label="绩效得分" width="120" align="center">
<template #default="scope">
<el-tag :type="getPerformanceTagType(scope.row.performanceScore)">
{{ scope.row.performanceScore.toFixed(1) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="机构详情" min-width="300">
<template #default="scope">
<div class="institution-details">
<el-tag
v-for="inst in scope.row.institutions"
:key="inst.id"
:type="getImageCountTagType(inst.imageCount)"
size="small"
style="margin: 2px"
>
{{ inst.name }} ({{ inst.imageCount }}张)
</el-tag>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 无数据提示 -->
<div v-else-if="availableMonths.length === 0" class="no-history-data">
<el-empty description="暂无历史统计数据">
<el-button type="primary" @click="saveCurrentMonthStats">保存当前月份数据</el-button>
</el-empty>
</div>
<!-- 选择月份提示 -->
<div v-else class="select-month-tip">
<el-alert
title="请选择要查看的历史月份"
type="info"
:closable="false"
show-icon
/>
</div>
</div>
</div>
</el-tab-pane>
<!-- 数据管理 -->
<el-tab-pane label="数据管理" name="dataManagement">
<div class="tab-content">
......@@ -355,8 +494,57 @@
</div>
<el-row :gutter="16">
<!-- 数据修复 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="data-management-card repair">
<div class="card-header">
<el-icon class="card-icon repair"><Tools /></el-icon>
<h4>数据修复</h4>
</div>
<div class="card-content">
<p>修复图片归属错误和机构权限验证问题。</p>
<div class="repair-options">
<div class="repair-item">
<span class="label">图片归属修复:</span>
<span class="description">修复图片显示在错误机构的问题</span>
</div>
<div class="repair-item">
<span class="label">权限验证修复:</span>
<span class="description">修复机构权限验证错误</span>
</div>
<div class="repair-item">
<span class="label">权限测试:</span>
<span class="description">测试图片上传权限验证</span>
</div>
<div class="repair-item">
<span class="label">ID修复:</span>
<span class="description">修复虚拟/真实机构ID问题</span>
</div>
</div>
</div>
<div class="card-actions">
<el-button type="warning" @click="fixImageOwnership" :loading="fixImageLoading">
<el-icon><Tools /></el-icon>
修复图片归属
</el-button>
<el-button type="info" @click="fixPermissionErrors" :loading="fixPermissionLoading">
<el-icon><Lock /></el-icon>
修复权限验证
</el-button>
<el-button type="primary" @click="testPermissions" :loading="testPermissionLoading" size="small">
<el-icon><Search /></el-icon>
测试权限
</el-button>
<el-button type="success" @click="fixVirtualRealIds" :loading="fixVirtualRealLoading" size="small">
<el-icon><Setting /></el-icon>
修复ID问题
</el-button>
</div>
</div>
</el-col>
<!-- 数据备份 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
<div class="data-management-card">
<div class="card-header">
<el-icon class="card-icon backup"><Download /></el-icon>
......@@ -389,7 +577,7 @@
</el-col>
<!-- 数据恢复 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
<div class="data-management-card">
<div class="card-header">
<el-icon class="card-icon restore"><Upload /></el-icon>
......@@ -429,7 +617,7 @@
</el-col>
<!-- 数据重置 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
<div class="data-management-card danger">
<div class="card-header">
<el-icon class="card-icon reset"><RefreshLeft /></el-icon>
......@@ -798,7 +986,11 @@ import {
Download,
FolderOpened,
RefreshLeft,
WarningFilled
WarningFilled,
Tools,
Lock,
Setting,
Delete
} from '@element-plus/icons-vue'
import { useAuthStore } from '@/store/auth'
import { useDataStore } from '@/store/data'
......@@ -859,6 +1051,17 @@ const exportLoading = ref(false)
const importLoading = ref(false)
const resetLoading = ref(false)
const selectedFile = ref(null)
const fixImageLoading = ref(false)
const fixPermissionLoading = ref(false)
const testPermissionLoading = ref(false)
const fixVirtualRealLoading = ref(false)
// 历史统计相关变量
const selectedHistoryMonth = ref('')
const selectedMonthData = ref(null)
const availableMonths = ref([])
const saveStatsLoading = ref(false)
const clearHistoryLoading = ref(false)
// 表单引用
const addUserFormRef = ref()
......@@ -2086,13 +2289,420 @@ const showResetConfirm = async () => {
}
/**
* 修复图片归属错误
*/
const fixImageOwnership = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复图片归属错误问题,包括:\n' +
'• 修复图片显示在错误机构的问题\n' +
'• 合并相似机构的图片到正确位置\n\n' +
'确定要继续吗?',
'修复图片归属',
{
type: 'warning',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
fixImageLoading.value = true
// 执行图片归属修复
const result = dataStore.fixImageOwnershipIssues()
if (result.fixed > 0) {
ElMessage.success(`图片归属修复完成!共修复了 ${result.fixed} 个问题`)
if (result.issues.length > 0) {
console.log('修复详情:', result.issues)
ElMessage.info(`修复详情:${result.issues.slice(0, 3).join(';')}${result.issues.length > 3 ? '...' : ''}`)
}
} else {
ElMessage.info('未发现图片归属问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复图片归属失败:', error)
ElMessage.error('修复图片归属失败!')
}
} finally {
fixImageLoading.value = false
}
}
/**
* 修复机构权限验证错误
*/
const fixPermissionErrors = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复机构权限验证错误,包括:\n' +
'• 修复重复的机构ID\n' +
'• 检查跨地区机构归属问题\n' +
'• 验证用户权限映射\n\n' +
'确定要继续吗?',
'修复权限验证',
{
type: 'info',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
fixPermissionLoading.value = true
// 执行权限验证修复
const result = dataStore.fixInstitutionPermissionErrors()
if (result.fixed > 0) {
ElMessage.success(`权限验证修复完成!共修复了 ${result.fixed} 个问题`)
if (result.issues.length > 0) {
console.log('修复详情:', result.issues)
ElMessage.info(`修复详情:${result.issues.slice(0, 3).join(';')}${result.issues.length > 3 ? '...' : ''}`)
}
} else {
ElMessage.info('未发现权限验证问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复权限验证失败:', error)
ElMessage.error('修复权限验证失败!')
}
} finally {
fixPermissionLoading.value = false
}
}
/**
* 测试图片上传权限验证
*/
const testPermissions = async () => {
try {
await ElMessageBox.confirm(
'此操作将测试所有用户的图片上传权限验证,包括:\n' +
'• 测试用户对自己机构的权限\n' +
'• 测试用户对其他机构的权限(应该被拒绝)\n' +
'• 检查权限验证逻辑的完整性\n\n' +
'确定要开始测试吗?',
'测试权限验证',
{
type: 'info',
confirmButtonText: '开始测试',
cancelButtonText: '取消'
}
)
testPermissionLoading.value = true
// 获取所有普通用户
const normalUsers = dataStore.getUsers().filter(user => user.role === 'user')
if (normalUsers.length === 0) {
ElMessage.warning('没有找到普通用户,无法进行权限测试')
return
}
console.log(`开始测试 ${normalUsers.length} 个用户的权限...`)
const allResults = []
let totalTests = 0
let passedTests = 0
for (const user of normalUsers) {
console.log(`\n测试用户: ${user.name}`)
const result = dataStore.testImageUploadPermissions(user.id)
allResults.push(result)
totalTests += result.totalTests
passedTests += result.passedTests
if (!result.success) {
console.warn(`用户 ${user.name} 的权限测试失败`)
result.results.forEach(r => {
if (!r.success) {
console.warn(` - ${r.institutionName}: ${r.error}`)
}
})
}
}
// 显示测试结果
const overallSuccess = passedTests === totalTests
if (overallSuccess) {
ElMessage.success(`权限验证测试完成!所有 ${totalTests} 项测试都通过了`)
} else {
ElMessage.warning(`权限验证测试完成!${passedTests}/${totalTests} 项测试通过`)
}
// 显示详细结果
console.log('\n=== 权限测试总结 ===')
console.log(`总测试数: ${totalTests}`)
console.log(`通过测试: ${passedTests}`)
console.log(`失败测试: ${totalTests - passedTests}`)
allResults.forEach(result => {
console.log(`\n用户 ${result.userName}:`)
console.log(` 测试数: ${result.totalTests}`)
console.log(` 通过数: ${result.passedTests}`)
if (!result.success) {
console.log(` 失败原因:`)
result.results.filter(r => !r.success).forEach(r => {
console.log(` - ${r.institutionName}: ${r.error}`)
})
}
})
// 如果有失败的测试,提供修复建议
if (!overallSuccess) {
ElMessage.info('发现权限验证问题,建议执行"修复权限验证"功能')
}
} catch (error) {
if (error !== 'cancel') {
console.error('权限测试失败:', error)
ElMessage.error('权限测试失败!')
}
} finally {
testPermissionLoading.value = false
}
}
/**
* 修复虚拟/真实机构ID问题
*/
const fixVirtualRealIds = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复虚拟机构和真实机构的ID问题,包括:\n' +
'• 为缺失ID的机构生成新ID\n' +
'• 修复ID格式问题\n' +
'• 检查并修复负责人关联\n' +
'• 验证数据结构完整性\n\n' +
'确定要开始修复吗?',
'修复机构ID问题',
{
type: 'warning',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
fixVirtualRealLoading.value = true
// 执行虚拟/真实机构ID修复
const result = dataStore.fixVirtualRealInstitutionIds()
if (result.fixed > 0) {
ElMessage.success(`机构ID修复完成!共修复了 ${result.fixed} 个问题`)
if (result.issues.length > 0) {
console.log('修复详情:', result.issues)
ElMessage.info(`修复详情:${result.issues.slice(0, 3).join(';')}${result.issues.length > 3 ? '...' : ''}`)
}
// 建议用户测试图片上传功能
setTimeout(() => {
ElMessage.info('建议现在测试图片上传功能是否正常')
}, 2000)
} else {
ElMessage.info('未发现需要修复的机构ID问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复机构ID失败:', error)
ElMessage.error('修复机构ID失败!')
}
} finally {
fixVirtualRealLoading.value = false
}
}
/**
* 历史统计相关方法
*/
/**
* 加载可用的历史月份
*/
const loadAvailableMonths = () => {
availableMonths.value = dataStore.getAvailableHistoryMonths()
console.log('可用历史月份:', availableMonths.value)
}
/**
* 保存当前月份统计数据
*/
const saveCurrentMonthStats = async () => {
try {
await ElMessageBox.confirm(
'确定要保存当前月份的统计数据吗?\n' +
'这将记录所有用户的当前绩效数据。',
'保存月度统计',
{
type: 'info',
confirmButtonText: '保存',
cancelButtonText: '取消'
}
)
saveStatsLoading.value = true
const success = dataStore.saveCurrentMonthStats()
if (success) {
ElMessage.success('当前月份统计数据保存成功!')
loadAvailableMonths() // 刷新可用月份列表
} else {
ElMessage.error('保存统计数据失败!')
}
} catch (error) {
if (error !== 'cancel') {
console.error('保存统计数据失败:', error)
ElMessage.error('保存统计数据失败!')
}
} finally {
saveStatsLoading.value = false
}
}
/**
* 加载指定月份的历史数据
*/
const loadHistoryMonth = (monthKey) => {
if (!monthKey) {
selectedMonthData.value = null
return
}
const monthData = dataStore.getMonthStats(monthKey)
if (monthData) {
selectedMonthData.value = monthData
console.log(`加载 ${monthKey} 月份数据:`, monthData)
} else {
ElMessage.error('加载历史数据失败!')
selectedMonthData.value = null
}
}
/**
* 删除选中月份的数据
*/
const deleteSelectedMonth = async () => {
if (!selectedHistoryMonth.value) return
try {
await ElMessageBox.confirm(
`确定要删除 ${formatMonthLabel(selectedHistoryMonth.value)} 的历史数据吗?\n` +
'此操作不可恢复!',
'删除历史数据',
{
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
}
)
const success = dataStore.deleteMonthStats(selectedHistoryMonth.value)
if (success) {
ElMessage.success('历史数据删除成功!')
selectedHistoryMonth.value = ''
selectedMonthData.value = null
loadAvailableMonths() // 刷新可用月份列表
} else {
ElMessage.error('删除历史数据失败!')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除历史数据失败:', error)
ElMessage.error('删除历史数据失败!')
}
}
}
/**
* 清空所有历史数据
*/
const clearHistoryStats = async () => {
try {
await ElMessageBox.confirm(
'确定要清空所有历史统计数据吗?\n' +
'此操作将删除所有月份的历史记录,不可恢复!',
'清空历史数据',
{
type: 'error',
confirmButtonText: '清空',
cancelButtonText: '取消'
}
)
clearHistoryLoading.value = true
const success = dataStore.clearAllHistoryStats()
if (success) {
ElMessage.success('所有历史数据已清空!')
selectedHistoryMonth.value = ''
selectedMonthData.value = null
availableMonths.value = []
} else {
ElMessage.error('清空历史数据失败!')
}
} catch (error) {
if (error !== 'cancel') {
console.error('清空历史数据失败:', error)
ElMessage.error('清空历史数据失败!')
}
} finally {
clearHistoryLoading.value = false
}
}
/**
* 格式化月份标签
*/
const formatMonthLabel = (monthKey) => {
const [year, month] = monthKey.split('-')
return `${year}${month}月`
}
/**
* 格式化日期时间
*/
const formatDateTime = (dateTimeString) => {
const date = new Date(dateTimeString)
return date.toLocaleString('zh-CN')
}
/**
* 获取绩效得分标签类型
*/
const getPerformanceTagType = (score) => {
if (score >= 8) return 'success'
if (score >= 6) return 'warning'
return 'danger'
}
/**
* 组件挂载时初始化
*/
onMounted(() => {
// 检查权限(数据和认证状态已在main.js中初始化)
if (!authStore.isAuthenticated || !authStore.isAdmin) {
router.push('/login')
return
}
console.log('管理员面板组件已挂载')
// 加载历史统计数据
loadAvailableMonths()
// 自动保存月度统计(如果需要)
dataStore.autoSaveMonthlyStats()
})
// 注册图标组件
......@@ -3122,6 +3732,52 @@ const iconComponents = {
background: linear-gradient(135deg, #f56c6c, #ff4d4f);
}
.data-management-card .card-icon.repair {
background: linear-gradient(135deg, #e6a23c, #faad14);
}
.data-management-card.repair {
border: 2px solid #e6a23c;
}
.data-management-card.repair .repair-options {
margin-top: 12px;
}
.data-management-card.repair .repair-item {
margin-bottom: 8px;
padding: 8px;
background: #fdf6ec;
border-radius: 6px;
border-left: 3px solid #e6a23c;
}
.data-management-card.repair .repair-item .label {
font-weight: 600;
color: #e6a23c;
font-size: 12px;
}
.data-management-card.repair .repair-item .description {
color: #606266;
font-size: 11px;
margin-left: 8px;
}
.data-management-card.repair .card-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.data-management-card.repair .card-actions .el-button {
width: 100%;
}
.data-management-card.repair .card-actions .el-button.el-button--small {
margin-top: 4px;
}
.data-management-card h4 {
margin: 0;
color: #303133;
......@@ -3316,4 +3972,115 @@ const iconComponents = {
font-size: 12px;
}
}
/* 历史统计样式 */
.history-stats-container {
padding: 20px;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
}
.history-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.history-subtitle {
margin: 0;
color: #606266;
font-size: 14px;
}
.month-filter {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
display: flex;
align-items: center;
}
.month-overview {
margin-bottom: 20px;
}
.overview-card {
background: white;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.overview-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.overview-title {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.overview-value {
font-size: 28px;
font-weight: 600;
color: #409eff;
margin-bottom: 4px;
}
.overview-value.small {
font-size: 12px;
font-weight: normal;
color: #606266;
}
.history-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.institution-details {
max-height: 100px;
overflow-y: auto;
}
.no-history-data,
.select-month-tip {
padding: 40px;
text-align: center;
}
@media (max-width: 768px) {
.history-header {
flex-direction: column;
gap: 15px;
}
.month-filter {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.overview-value {
font-size: 24px;
}
}
</style>
\ No newline at end of file
......@@ -76,61 +76,7 @@
</el-col>
</el-row>
<!-- 紧急修复功能区 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<div class="emergency-section" style="background: #fff2f0; border: 2px solid #ff4d4f; border-radius: 8px; padding: 15px; margin-bottom: 20px;">
<h3 style="color: #ff4d4f; margin: 0 0 15px 0;">🚨 紧急修复工具</h3>
<el-space wrap>
<el-button type="danger" size="large" @click="emergencyFix">
🚨 紧急修复
</el-button>
<el-button type="info" @click="comprehensiveCheck">
🔍 完整性检查
</el-button>
<el-button type="success" @click="autoFix">
🔧 一键修复
</el-button>
</el-space>
</div>
</el-col>
</el-row>
<!-- 调试工具区 -->
<el-row :gutter="20" style="margin-top: 10px;">
<el-col :span="24">
<el-collapse>
<el-collapse-item title="🔧 调试工具" name="debug-tools">
<el-space wrap>
<el-button type="warning" @click="debugData">
调试数据
</el-button>
<el-button type="danger" @click="fixDuplicateIds">
修复重复ID
</el-button>
<el-button type="info" @click="cleanupExampleData">
清理示例数据
</el-button>
<el-button type="success" @click="debugImageUpload">
调试图片上传
</el-button>
<el-button type="primary" @click="fixDataStructure">
修复数据结构
</el-button>
<el-button type="danger" @click="fixDataOwnership">
修复数据归属
</el-button>
<el-button type="warning" @click="securityDiagnosis">
安全诊断
</el-button>
<el-button type="danger" @click="deepDataInvestigation">
深度数据调查
</el-button>
</el-space>
</el-collapse-item>
</el-collapse>
</el-col>
</el-row>
</div>
<!-- 机构列表 -->
......@@ -425,18 +371,15 @@ const beforeUpload = (file, institutionId) => {
return false
}
// 🔒 只在用户自己的机构中检测重复图片
const isDuplicate = userInstitutions.some(inst =>
inst.images.some(img =>
img.name === file.name && img.size === file.size
)
)
if (isDuplicate) {
ElMessage.error('重复图片无法上传!')
return false
// 🔍 使用新的重复检测功能(暂时创建临时图片数据进行检测)
const tempImageData = {
name: file.name,
size: file.size,
url: 'temp_url_for_detection' // 临时URL,实际检测会在压缩后进行
}
// 注意:这里只是基础检测,完整的重复检测会在实际上传时进行
const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5
......@@ -561,6 +504,20 @@ const handleImageUpload = (uploadFile, institutionId) => {
console.log('上传前机构图片数量:', institution.images.length)
console.log('上传前localStorage数据:', localStorage.getItem('score_system_institutions'))
// 🔍 在实际上传前进行重复检测
const duplicateCheck = dataStore.detectDuplicateImage(imageData, authStore.currentUser.id)
if (duplicateCheck.isDuplicate) {
if (!duplicateCheck.allowUpload) {
// 完全重复,禁止上传
ElMessage.error(duplicateCheck.message)
return
} else {
// 轻微编辑或同名不同内容,显示警告但允许上传
ElMessage.warning(duplicateCheck.message)
}
}
const result = dataStore.addImageToInstitution(institutionId, imageData, authStore.currentUser.id)
if (result) {
......@@ -669,729 +626,23 @@ const refreshData = () => {
ElMessage.success('数据刷新成功!')
}
/**
* 调试数据状态
*/
const debugData = () => {
console.log('=== 数据调试信息 ===')
// 🔒 安全检查:只有管理员才能看到所有机构数据
const isAdmin = authStore.currentUser.role === 'admin'
const allInstitutions = isAdmin ? dataStore.getInstitutions() : dataStore.getInstitutionsByUserId(authStore.currentUser.id)
console.log(isAdmin ? '管理员查看所有机构数量:' : '用户查看自己的机构数量:', allInstitutions.length)
// 检查机构ID重复
const institutionIds = allInstitutions.map(inst => inst.institutionId)
const duplicateIds = institutionIds.filter((id, index) => institutionIds.indexOf(id) !== index)
if (duplicateIds.length > 0) {
console.error('🚨 发现重复的机构ID:', duplicateIds)
ElMessage.error(`发现重复的机构ID: ${duplicateIds.join(', ')}`)
// 显示重复机构的详细信息
duplicateIds.forEach(duplicateId => {
const duplicateInstitutions = allInstitutions.filter(inst => inst.institutionId === duplicateId)
console.error(`机构ID ${duplicateId} 重复:`, duplicateInstitutions.map(inst => ({
id: inst.id,
name: inst.name,
ownerId: inst.ownerId,
imageCount: inst.images.length
})))
})
} else {
console.log('✅ 没有发现重复的机构ID')
}
// 检查当前用户的机构
const userInstitutions = dataStore.getInstitutionsByUserId(currentUser.value.id)
console.log('当前用户机构数量:', userInstitutions.length)
userInstitutions.forEach(inst => {
console.log(`机构 ${inst.name} (ID:${inst.institutionId}, 内部ID:${inst.id}):`, {
imagesCount: inst.images.length,
images: inst.images.map(img => ({
id: img.id,
name: img.name,
size: img.size
}))
})
})
// 检查localStorage数据
const savedData = localStorage.getItem('score_system_institutions')
if (savedData) {
const institutions = JSON.parse(savedData)
console.log('localStorage中的机构数据:')
institutions.forEach(inst => {
console.log(`机构 ${inst.name} (ID:${inst.institutionId}, 内部ID:${inst.id}):`, {
imagesCount: inst.images?.length || 0,
images: inst.images?.map(img => ({
id: img.id,
name: img.name,
size: img.size
})) || []
})
})
}
ElMessage.info('调试信息已输出到控制台,请查看浏览器控制台')
}
/**
* 修复重复的机构ID
*/
const fixDuplicateIds = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复重复的机构ID,可能会改变某些机构的ID。确定要继续吗?',
'修复重复ID',
{
type: 'warning',
confirmButtonText: '确定修复',
cancelButtonText: '取消'
}
)
const result = dataStore.fixDuplicateInstitutionIds()
if (result.fixed > 0) {
ElMessage.success(`修复完成!共修复了 ${result.fixed} 个重复的机构ID`)
console.log('修复结果:', result)
// 刷新页面数据
nextTick(() => {
// 强制重新加载数据
window.location.reload()
})
} else {
ElMessage.info('没有发现需要修复的重复ID')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复失败:', error)
ElMessage.error('修复失败: ' + error.message)
}
}
}
/**
* 清理示例数据
*/
const cleanupExampleData = async () => {
try {
await ElMessageBox.confirm(
'此操作将清理所有示例用户和示例机构(如A、B、C等),只保留真实的新增机构。确定要继续吗?',
'清理示例数据',
{
type: 'warning',
confirmButtonText: '确定清理',
cancelButtonText: '取消'
}
)
const result = dataStore.cleanupExampleData()
ElMessage.success(`清理完成!移除了 ${result.removedUsers} 个示例用户和 ${result.removedInstitutions} 个示例机构`)
console.log('清理结果:', result)
// 刷新页面数据
nextTick(() => {
// 强制重新加载数据
window.location.reload()
})
} catch (error) {
if (error !== 'cancel') {
console.error('清理失败:', error)
ElMessage.error('清理失败: ' + error.message)
}
}
}
/**
* 调试图片上传问题
*/
const debugImageUpload = () => {
console.log('=== 调试图片上传问题 ===')
// 🔒 安全检查:普通用户只能看到自己的机构
const isAdmin = authStore.currentUser.role === 'admin'
const allInstitutions = isAdmin ? dataStore.getInstitutions() : []
const userInstitutions = dataStore.getInstitutionsByUserId(authStore.currentUser.id)
if (isAdmin) {
console.log('管理员 - 所有机构数量:', allInstitutions.length)
}
console.log('用户机构数量:', userInstitutions.length)
// 检查每个机构的数据结构
userInstitutions.forEach((inst, index) => {
console.log(`机构 ${index + 1}: ${inst.name}`)
console.log(' - ID:', inst.id)
console.log(' - 机构ID:', inst.institutionId)
console.log(' - 负责人:', inst.ownerId)
console.log(' - images属性存在:', 'images' in inst)
console.log(' - images类型:', typeof inst.images)
console.log(' - images是数组:', Array.isArray(inst.images))
console.log(' - images长度:', inst.images?.length || 0)
console.log(' - images内容:', inst.images)
console.log(' ---')
})
// 检查localStorage中的数据
const savedData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]')
console.log('localStorage中的机构数量:', savedData.length)
savedData.forEach((inst, index) => {
if (inst.ownerId === authStore.currentUser.id) {
console.log(`localStorage机构 ${index + 1}: ${inst.name}`)
console.log(' - ID:', inst.id)
console.log(' - 机构ID:', inst.institutionId)
console.log(' - images属性存在:', 'images' in inst)
console.log(' - images类型:', typeof inst.images)
console.log(' - images是数组:', Array.isArray(inst.images))
console.log(' - images长度:', inst.images?.length || 0)
console.log(' - images内容:', inst.images)
console.log(' ---')
}
})
// 检查响应式数据
console.log('响应式数据检查:')
console.log(' - userInstitutions.value:', userInstitutions.value)
console.log(' - filteredInstitutions.value:', filteredInstitutions.value)
ElMessage.info('图片上传调试信息已输出到控制台')
}
/**
* 修复数据结构
*/
const fixDataStructure = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复所有机构的数据结构,确保图片上传功能正常。确定要继续吗?',
'修复数据结构',
{
type: 'info',
confirmButtonText: '确定修复',
cancelButtonText: '取消'
}
)
const result = dataStore.fixInstitutionDataStructure()
if (result.fixed > 0) {
ElMessage.success(`修复完成!共修复了 ${result.fixed} 个机构的数据结构`)
console.log('修复结果:', result)
// 刷新页面数据
nextTick(() => {
// 强制重新加载数据
dataStore.loadFromStorage()
ElMessage.info('数据已重新加载,请尝试上传图片')
})
} else {
ElMessage.info('所有机构数据结构正常,无需修复')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复失败:', error)
ElMessage.error('修复失败: ' + error.message)
}
}
}
/**
* 修复数据归属问题
*/
const fixDataOwnership = async () => {
try {
await ElMessageBox.confirm(
'此操作将修复数据归属问题,包括清理无效的机构归属和图片数据。确定要继续吗?',
'修复数据归属',
{
type: 'warning',
confirmButtonText: '确定修复',
cancelButtonText: '取消'
}
)
const result = dataStore.fixDataOwnership()
if (result.fixed > 0) {
ElMessage.success(`修复完成!共修复了 ${result.fixed} 个数据归属问题`)
console.log('修复结果:', result)
if (result.issues.length > 0) {
console.warn('发现的问题:', result.issues)
ElMessage.warning(`发现 ${result.issues.length} 个数据问题,已自动修复`)
}
// 刷新页面数据
nextTick(() => {
dataStore.loadFromStorage()
ElMessage.info('数据已重新加载')
})
} else {
ElMessage.info('数据归属正常,无需修复')
}
} catch (error) {
if (error !== 'cancel') {
console.error('修复失败:', error)
ElMessage.error('修复失败: ' + error.message)
}
}
}
/**
* 安全诊断
*/
const securityDiagnosis = () => {
console.log('=== 🔒 安全诊断报告 ===')
const currentUserId = authStore.currentUser.id
const isAdmin = authStore.currentUser.role === 'admin'
// 🔒 安全检查:只有管理员才能进行全系统安全诊断
const allInstitutions = isAdmin ? dataStore.getInstitutions() : dataStore.getInstitutionsByUserId(currentUserId)
const userInstitutions = dataStore.getInstitutionsByUserId(currentUserId)
console.log('👤 当前用户:', {
id: currentUserId,
name: authStore.currentUser.name,
role: authStore.currentUser.role
})
console.log('🏢 机构权限检查:')
console.log(` - 系统总机构数: ${allInstitutions.length}`)
console.log(` - 用户可访问机构数: ${userInstitutions.length}`)
// 检查是否有权限泄露
const unauthorizedAccess = []
allInstitutions.forEach(inst => {
if (inst.ownerId !== currentUserId && inst.ownerId !== null) {
unauthorizedAccess.push({
institutionName: inst.name,
institutionId: inst.institutionId,
actualOwner: inst.ownerId
})
}
})
if (unauthorizedAccess.length > 0) {
console.warn('⚠️ 发现潜在的权限泄露:')
unauthorizedAccess.forEach(item => {
console.warn(` - 机构 "${item.institutionName}" (ID: ${item.institutionId}) 属于用户 ${item.actualOwner}`)
})
} else {
console.log('✅ 权限检查通过,无权限泄露')
}
// 检查机构ID重复
const institutionIds = allInstitutions.map(inst => inst.institutionId)
const duplicateIds = institutionIds.filter((id, index) => institutionIds.indexOf(id) !== index)
if (duplicateIds.length > 0) {
console.warn('⚠️ 发现重复的机构ID:', duplicateIds)
} else {
console.log('✅ 机构ID唯一性检查通过')
}
// 检查图片归属
let totalImages = 0
let orphanImages = 0
userInstitutions.forEach(inst => {
if (inst.images && inst.images.length > 0) {
totalImages += inst.images.length
inst.images.forEach(img => {
if (!img.id || !img.url) {
orphanImages++
}
})
}
})
console.log('📸 图片数据检查:')
console.log(` - 用户总图片数: ${totalImages}`)
console.log(` - 无效图片数: ${orphanImages}`)
if (orphanImages > 0) {
console.warn(`⚠️ 发现 ${orphanImages} 张无效图片`)
} else {
console.log('✅ 图片数据完整性检查通过')
}
// 生成安全报告
const securityReport = {
timestamp: new Date().toISOString(),
userId: currentUserId,
totalInstitutions: allInstitutions.length,
userInstitutions: userInstitutions.length,
unauthorizedAccess: unauthorizedAccess.length,
duplicateIds: duplicateIds.length,
totalImages: totalImages,
orphanImages: orphanImages,
securityScore: calculateSecurityScore(unauthorizedAccess.length, duplicateIds.length, orphanImages)
}
console.log('📊 安全评分:', securityReport.securityScore)
console.log('=== 诊断完成 ===')
// 显示结果
if (securityReport.securityScore >= 90) {
ElMessage.success(`安全诊断完成!安全评分: ${securityReport.securityScore}/100`)
} else if (securityReport.securityScore >= 70) {
ElMessage.warning(`安全诊断完成!安全评分: ${securityReport.securityScore}/100,建议修复发现的问题`)
} else {
ElMessage.error(`安全诊断完成!安全评分: ${securityReport.securityScore}/100,存在严重安全问题!`)
}
}
/**
* 计算安全评分
*/
const calculateSecurityScore = (unauthorizedCount, duplicateCount, orphanCount) => {
let score = 100
// 权限泄露扣分最严重
score -= unauthorizedCount * 30
// ID重复扣分
score -= duplicateCount * 20
// 无效图片扣分
score -= orphanCount * 5
return Math.max(0, score)
}
/**
* 深度数据调查 - 专门调查陈锐屏和余芳菲的数据泄露问题
*/
const deepDataInvestigation = () => {
console.log('🔍 === 深度数据调查开始 ===')
// 🔒 安全检查:只有管理员才能执行深度数据调查
const isAdmin = authStore.currentUser.role === 'admin'
if (!isAdmin) {
console.error('❌ 权限不足:只有管理员才能执行深度数据调查')
ElMessage.error('权限不足:只有管理员才能执行此操作')
return
}
console.log('调查目标:陈锐屏 -> 余芳菲 的图片归属错误问题')
// 获取所有用户和机构数据(管理员权限)
const allUsers = dataStore.getUsers()
const allInstitutions = dataStore.getInstitutions()
// 查找目标用户
const chenRuiPing = allUsers.find(user => user.name === '陈锐屏')
const yuFangFei = allUsers.find(user => user.name === '余芳菲' || user.name === '余芳飞')
console.log('👤 目标用户信息:')
console.log('陈锐屏:', chenRuiPing ? {
id: chenRuiPing.id,
name: chenRuiPing.name,
phone: chenRuiPing.phone
} : '❌ 未找到')
console.log('余芳菲:', yuFangFei ? {
id: yuFangFei.id,
name: yuFangFei.name,
phone: yuFangFei.phone
} : '❌ 未找到')
if (!chenRuiPing || !yuFangFei) {
console.error('❌ 无法找到目标用户,调查终止')
ElMessage.error('无法找到目标用户')
return
}
// 查找目标机构
const wuHuaQu = allInstitutions.find(inst =>
inst.name.includes('五华区长青口腔诊所') ||
inst.name.includes('长青口腔')
)
const dalianXiGang = allInstitutions.find(inst =>
inst.name.includes('大连西岗悦佳口腔诊所') ||
inst.name.includes('悦佳口腔')
)
console.log('🏢 目标机构信息:')
console.log('五华区长青口腔诊所:', wuHuaQu ? {
id: wuHuaQu.id,
institutionId: wuHuaQu.institutionId,
name: wuHuaQu.name,
ownerId: wuHuaQu.ownerId,
imagesCount: wuHuaQu.images?.length || 0
} : '❌ 未找到')
console.log('大连西岗悦佳口腔诊所:', dalianXiGang ? {
id: dalianXiGang.id,
institutionId: dalianXiGang.institutionId,
name: dalianXiGang.name,
ownerId: dalianXiGang.ownerId,
imagesCount: dalianXiGang.images?.length || 0
} : '❌ 未找到')
// 检查机构归属关系
console.log('🔗 机构归属关系检查:')
if (wuHuaQu) {
const shouldBelongTo = chenRuiPing.id
const actualBelongTo = wuHuaQu.ownerId
console.log(`五华区长青口腔诊所 应该属于: ${shouldBelongTo} (陈锐屏)`)
console.log(`五华区长青口腔诊所 实际属于: ${actualBelongTo}`)
if (shouldBelongTo !== actualBelongTo) {
console.error('❌ 发现归属错误!五华区长青口腔诊所归属不正确')
} else {
console.log('✅ 五华区长青口腔诊所归属正确')
}
}
if (dalianXiGang) {
const shouldBelongTo = yuFangFei.id
const actualBelongTo = dalianXiGang.ownerId
console.log(`大连西岗悦佳口腔诊所 应该属于: ${shouldBelongTo} (余芳菲)`)
console.log(`大连西岗悦佳口腔诊所 实际属于: ${actualBelongTo}`)
if (shouldBelongTo !== actualBelongTo) {
console.error('❌ 发现归属错误!大连西岗悦佳口腔诊所归属不正确')
} else {
console.log('✅ 大连西岗悦佳口腔诊所归属正确')
}
}
// 检查图片数据
console.log('📸 图片数据详细分析:')
if (wuHuaQu && wuHuaQu.images && wuHuaQu.images.length > 0) {
console.log(`五华区长青口腔诊所 图片列表 (${wuHuaQu.images.length}张):`)
wuHuaQu.images.forEach((img, index) => {
console.log(` 图片 ${index + 1}:`, {
id: img.id,
name: img.name,
uploadTime: img.uploadTime,
size: img.size
})
})
}
if (dalianXiGang && dalianXiGang.images && dalianXiGang.images.length > 0) {
console.log(`大连西岗悦佳口腔诊所 图片列表 (${dalianXiGang.images.length}张):`)
dalianXiGang.images.forEach((img, index) => {
console.log(` 图片 ${index + 1}:`, {
id: img.id,
name: img.name,
uploadTime: img.uploadTime,
size: img.size
})
})
}
// 检查localStorage原始数据
console.log('💾 localStorage 原始数据检查:')
const savedInstitutions = localStorage.getItem('score_system_institutions')
const savedUsers = localStorage.getItem('score_system_users')
if (savedInstitutions) {
const institutions = JSON.parse(savedInstitutions)
const wuHuaQu_saved = institutions.find(inst => inst.name.includes('五华区长青口腔诊所'))
const dalianXiGang_saved = institutions.find(inst => inst.name.includes('大连西岗悦佳口腔诊所'))
console.log('localStorage中的五华区长青口腔诊所:', wuHuaQu_saved)
console.log('localStorage中的大连西岗悦佳口腔诊所:', dalianXiGang_saved)
}
console.log('🔍 === 深度数据调查完成 ===')
ElMessage.info('深度数据调查完成,请查看控制台详细信息')
}
/**
* 紧急修复数据泄露问题
*/
const emergencyFix = async () => {
try {
await ElMessageBox.confirm(
'🚨 这是紧急修复功能,将立即修复陈锐屏和余芳菲之间的数据泄露问题。确定要继续吗?',
'紧急修复数据泄露',
{
type: 'error',
confirmButtonText: '立即修复',
cancelButtonText: '取消'
}
)
console.log('🚨 开始执行紧急修复...')
const result = await dataStore.emergencyFixDataLeak()
if (result.fixed > 0) {
ElMessage.success(`🚨 紧急修复完成!共修复了 ${result.fixed} 个严重问题`)
console.log('紧急修复结果:', result)
// 显示修复详情
if (result.issues.length > 0) {
console.log('修复的问题:')
result.issues.forEach((issue, index) => {
console.log(`${index + 1}. ${issue}`)
})
ElMessage.info(`修复详情:${result.issues.join(';')}`)
}
// 强制刷新页面数据
nextTick(() => {
dataStore.loadFromStorage()
ElMessage.success('数据已重新加载,数据泄露问题已解决!')
// 自动执行安全诊断验证修复结果
setTimeout(() => {
securityDiagnosis()
}, 1000)
})
} else {
ElMessage.info('未发现需要紧急修复的数据泄露问题')
}
} catch (error) {
if (error !== 'cancel') {
console.error('紧急修复失败:', error)
ElMessage.error('紧急修复失败: ' + error.message)
}
}
}
/**
* 全面数据完整性检查
*/
const comprehensiveCheck = () => {
console.log('🔍 开始全面数据完整性检查...')
const report = dataStore.comprehensiveDataIntegrityCheck()
console.log('📊 数据完整性报告:', report)
// 显示检查结果
if (report.integrityScore >= 90) {
ElMessage.success(`数据完整性检查完成!评分: ${report.integrityScore}/100 - 系统状态良好`)
} else if (report.integrityScore >= 70) {
ElMessage.warning(`数据完整性检查完成!评分: ${report.integrityScore}/100 - 发现 ${report.issues.length} 个问题`)
} else {
ElMessage.error(`数据完整性检查完成!评分: ${report.integrityScore}/100 - 发现严重问题!`)
}
// 显示详细统计信息
ElMessage.info(`统计信息:用户 ${report.statistics.totalUsers} 个,机构 ${report.statistics.totalInstitutions} 个,图片 ${report.statistics.totalImages} 张`)
if (report.issues.length > 0) {
console.warn('发现的问题:')
report.issues.forEach((issue, index) => {
console.warn(`${index + 1}. ${issue}`)
})
if (report.fixes.length > 0) {
console.log('修复建议:')
report.fixes.forEach((fix, index) => {
console.log(`${index + 1}. ${fix}`)
})
}
}
ElMessage.info('详细报告已输出到控制台,请查看')
}
/**
* 一键修复所有问题
*/
const autoFix = async () => {
try {
await ElMessageBox.confirm(
'🔧 这将自动执行所有修复操作,包括:\n' +
'• 修复数据结构\n' +
'• 修复数据归属\n' +
'• 紧急修复数据泄露\n' +
'• 清理无效数据\n\n' +
'确定要继续吗?',
'一键修复所有问题',
{
type: 'warning',
confirmButtonText: '开始修复',
cancelButtonText: '取消'
}
)
console.log('🔧 开始一键修复...')
let totalFixed = 0
const fixResults = []
// 1. 修复数据结构
console.log('1️⃣ 修复数据结构...')
const structureResult = dataStore.fixInstitutionDataStructure()
totalFixed += structureResult.fixed
if (structureResult.fixed > 0) {
fixResults.push(`数据结构: 修复了 ${structureResult.fixed} 个问题`)
}
// 2. 修复数据归属
console.log('2️⃣ 修复数据归属...')
const ownershipResult = dataStore.fixDataOwnership()
totalFixed += ownershipResult.fixed
if (ownershipResult.fixed > 0) {
fixResults.push(`数据归属: 修复了 ${ownershipResult.fixed} 个问题`)
}
// 3. 紧急修复数据泄露
console.log('3️⃣ 紧急修复数据泄露...')
const emergencyResult = await dataStore.emergencyFixDataLeak()
totalFixed += emergencyResult.fixed
if (emergencyResult.fixed > 0) {
fixResults.push(`数据泄露: 修复了 ${emergencyResult.fixed} 个问题`)
}
// 4. 修复重复ID
console.log('4️⃣ 修复重复ID...')
const duplicateResult = dataStore.fixDuplicateInstitutionIds()
totalFixed += duplicateResult.fixed
if (duplicateResult.fixed > 0) {
fixResults.push(`重复ID: 修复了 ${duplicateResult.fixed} 个问题`)
}
if (totalFixed > 0) {
ElMessage.success(`🎉 一键修复完成!共修复了 ${totalFixed} 个问题`)
if (fixResults.length > 0) {
console.log('修复详情:')
fixResults.forEach((result, index) => {
console.log(`${index + 1}. ${result}`)
})
ElMessage.info(`修复详情:${fixResults.join(';')}`)
}
// 强制刷新页面数据
nextTick(() => {
dataStore.loadFromStorage()
ElMessage.success('数据已重新加载!')
// 自动执行完整性检查验证修复结果
setTimeout(() => {
comprehensiveCheck()
}, 1000)
})
} else {
ElMessage.info('✅ 系统状态良好,无需修复')
}
} catch (error) {
if (error !== 'cancel') {
console.error('一键修复失败:', error)
ElMessage.error('一键修复失败: ' + error.message)
}
}
}
/**
* 退出登录
......
# 绩效计分系统启动脚本
# 用于启动前端服务器和WebSocket服务器
Write-Host "=== 绩效计分系统启动脚本 ===" -ForegroundColor Green
# 检查Node.js是否安装
try {
$nodeVersion = node --version
Write-Host "Node.js版本: $nodeVersion" -ForegroundColor Green
} catch {
Write-Host "错误: 未找到Node.js,请先安装Node.js" -ForegroundColor Red
exit 1
}
# 检查npm是否安装
try {
$npmVersion = npm --version
Write-Host "npm版本: $npmVersion" -ForegroundColor Green
} catch {
Write-Host "错误: 未找到npm" -ForegroundColor Red
exit 1
}
# 停止可能正在运行的进程
Write-Host "停止现有进程..." -ForegroundColor Yellow
Get-Process -Name "node" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
# 等待进程完全停止
Start-Sleep -Seconds 2
# 检查端口是否被占用
$port5173 = Get-NetTCPConnection -LocalPort 5173 -ErrorAction SilentlyContinue
$port3000 = Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue
if ($port5173) {
Write-Host "警告: 端口5173仍被占用" -ForegroundColor Yellow
}
if ($port3000) {
Write-Host "警告: 端口3000仍被占用" -ForegroundColor Yellow
}
# 安装依赖(如果需要)
if (-not (Test-Path "node_modules")) {
Write-Host "安装依赖..." -ForegroundColor Yellow
npm install
if ($LASTEXITCODE -ne 0) {
Write-Host "错误: 依赖安装失败" -ForegroundColor Red
exit 1
}
}
Write-Host "启动WebSocket服务器..." -ForegroundColor Yellow
$websocketJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
node websocket-server.cjs
}
# 等待WebSocket服务器启动
Start-Sleep -Seconds 3
Write-Host "启动前端服务器..." -ForegroundColor Yellow
$frontendJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
npx vite --host 0.0.0.0 --port 5173
}
# 等待前端服务器启动
Start-Sleep -Seconds 5
# 检查服务状态
Write-Host "检查服务状态..." -ForegroundColor Yellow
try {
$frontendResponse = Invoke-WebRequest -Uri "http://localhost:5173" -Method HEAD -TimeoutSec 5 -ErrorAction Stop
Write-Host "✓ 前端服务器运行正常 (http://localhost:5173)" -ForegroundColor Green
} catch {
Write-Host "✗ 前端服务器启动失败" -ForegroundColor Red
}
try {
$websocketResponse = Invoke-WebRequest -Uri "http://localhost:3000" -Method HEAD -TimeoutSec 5 -ErrorAction Stop
Write-Host "✓ WebSocket服务器运行正常 (http://localhost:3000)" -ForegroundColor Green
} catch {
Write-Host "✗ WebSocket服务器启动失败" -ForegroundColor Red
}
Write-Host ""
Write-Host "=== 系统启动完成 ===" -ForegroundColor Green
Write-Host "前端应用: http://localhost:5173" -ForegroundColor Cyan
Write-Host "状态检查: http://localhost:5173/status.html" -ForegroundColor Cyan
Write-Host "同步测试: http://localhost:5173/sync-test.html" -ForegroundColor Cyan
Write-Host ""
Write-Host "按 Ctrl+C 停止所有服务" -ForegroundColor Yellow
# 监控服务状态
try {
while ($true) {
Start-Sleep -Seconds 30
# 检查作业状态
if ($websocketJob.State -eq "Failed" -or $websocketJob.State -eq "Stopped") {
Write-Host "WebSocket服务器已停止,重新启动..." -ForegroundColor Yellow
$websocketJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
node websocket-server.cjs
}
}
if ($frontendJob.State -eq "Failed" -or $frontendJob.State -eq "Stopped") {
Write-Host "前端服务器已停止,重新启动..." -ForegroundColor Yellow
$frontendJob = Start-Job -ScriptBlock {
Set-Location $using:PWD
npx vite --host 0.0.0.0 --port 5173
}
}
}
} finally {
Write-Host "停止所有服务..." -ForegroundColor Yellow
Stop-Job $websocketJob, $frontendJob -ErrorAction SilentlyContinue
Remove-Job $websocketJob, $frontendJob -ErrorAction SilentlyContinue
Get-Process -Name "node" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Write-Host "所有服务已停止" -ForegroundColor Green
}
console.log('Node.js version:', process.version);
console.log('Current directory:', process.cwd());
console.log('Environment test successful');
/**
* 测试修复功能的脚本
* 用于验证图片归属修复和权限验证修复是否正常工作
*/
// 模拟测试数据
const testData = {
users: [
{ id: 'user1', name: '陈锐屏', role: 'user' },
{ id: 'user2', name: '余芳菲', role: 'user' },
{ id: 'user3', name: '测试用户', role: 'user' },
{ id: 'admin1', name: '管理员', role: 'admin' }
],
institutions: [
{
id: 'inst1',
institutionId: 'INST001',
name: '五华区长青口腔诊所',
ownerId: 'user1',
images: [
{ id: 'img1', name: '测试图片1.jpg', url: 'test1.jpg' },
{ id: 'img2', name: '测试图片2.jpg', url: 'test2.jpg' }
]
},
{
id: 'inst2',
institutionId: 'INST002',
name: '昆明市五华区爱雅仕口腔诊所',
ownerId: 'user1',
images: [
{ id: 'img3', name: '错误归属图片.jpg', url: 'test3.jpg' }
]
},
{
id: 'inst3',
institutionId: 'INST003',
name: '昆明美云口腔医院有限公司安宁口腔诊所',
ownerId: 'user2',
images: []
},
{
id: 'inst4',
institutionId: 'INST004',
name: '兰州至善振林康美口腔医疗有限责任公司',
ownerId: 'user3',
images: []
},
// 测试重复ID的情况
{
id: 'inst5',
institutionId: 'INST001', // 重复的机构ID
name: '重复ID测试机构',
ownerId: 'user3',
images: []
}
]
}
/**
* 测试图片归属修复功能
*/
function testImageOwnershipFix() {
console.log('🧪 测试图片归属修复功能...')
// 模拟问题:五华区长青口腔诊所的图片显示在爱雅仕口腔诊所
const problems = []
// 检查是否有相似机构名称
const wuhuaInstitutions = testData.institutions.filter(inst =>
inst.name.includes('五华区') && inst.name.includes('口腔')
)
if (wuhuaInstitutions.length > 1) {
console.log('发现多个五华区口腔机构:', wuhuaInstitutions.map(i => i.name))
problems.push('存在多个相似机构,可能导致图片归属混乱')
}
// 检查图片分布
wuhuaInstitutions.forEach(inst => {
console.log(`机构 ${inst.name}${inst.images.length} 张图片`)
})
return {
success: problems.length === 0,
problems: problems,
suggestions: problems.length > 0 ? ['合并相似机构的图片', '确保图片归属正确'] : []
}
}
/**
* 测试权限验证修复功能
*/
function testPermissionFix() {
console.log('🧪 测试权限验证修复功能...')
const problems = []
// 检查机构ID重复
const institutionIds = testData.institutions.map(inst => inst.institutionId)
const duplicateIds = institutionIds.filter((id, index) => institutionIds.indexOf(id) !== index)
if (duplicateIds.length > 0) {
console.log('发现重复的机构ID:', duplicateIds)
problems.push(`重复的机构ID: ${duplicateIds.join(', ')}`)
}
// 检查用户权限映射
testData.users.forEach(user => {
if (user.role === 'user') {
const userInstitutions = testData.institutions.filter(inst => inst.ownerId === user.id)
console.log(`用户 ${user.name} 负责 ${userInstitutions.length} 个机构`)
// 检查跨地区机构
const regions = new Set()
userInstitutions.forEach(inst => {
if (inst.name.includes('五华区') || inst.name.includes('昆明')) {
regions.add('昆明')
} else if (inst.name.includes('兰州')) {
regions.add('兰州')
} else if (inst.name.includes('安宁')) {
regions.add('安宁')
}
})
if (regions.size > 1) {
console.log(`用户 ${user.name} 负责跨地区机构: ${Array.from(regions).join(', ')}`)
problems.push(`用户 ${user.name} 负责跨地区机构`)
}
}
})
return {
success: problems.length === 0,
problems: problems,
suggestions: problems.length > 0 ? ['修复重复机构ID', '检查用户权限映射'] : []
}
}
/**
* 测试图片上传权限验证
*/
function testImageUploadPermission(userId, institutionId) {
console.log(`🧪 测试用户 ${userId} 对机构 ${institutionId} 的上传权限...`)
const user = testData.users.find(u => u.id === userId)
const institution = testData.institutions.find(i => i.id === institutionId)
if (!user) {
return { success: false, error: '用户不存在' }
}
if (!institution) {
return { success: false, error: '机构不存在' }
}
if (institution.ownerId !== userId) {
return {
success: false,
error: `权限验证失败: 用户 ${user.name} 无权操作机构 ${institution.name}`
}
}
return {
success: true,
message: `用户 ${user.name} 有权操作机构 ${institution.name}`
}
}
/**
* 运行所有测试
*/
function runAllTests() {
console.log('🚀 开始运行所有修复功能测试...')
const results = {
imageOwnership: testImageOwnershipFix(),
permissionFix: testPermissionFix(),
uploadPermissions: []
}
// 测试图片上传权限
console.log('\n🧪 测试图片上传权限...')
// 测试正常权限
results.uploadPermissions.push({
test: '用户1访问自己的机构',
result: testImageUploadPermission('user1', 'inst1')
})
// 测试跨用户权限(应该失败)
results.uploadPermissions.push({
test: '用户1访问用户2的机构',
result: testImageUploadPermission('user1', 'inst3')
})
// 测试不存在的机构
results.uploadPermissions.push({
test: '访问不存在的机构',
result: testImageUploadPermission('user1', 'nonexistent')
})
// 输出测试结果
console.log('\n📊 测试结果总结:')
console.log('图片归属修复:', results.imageOwnership.success ? '✅ 通过' : '❌ 失败')
console.log('权限验证修复:', results.permissionFix.success ? '✅ 通过' : '❌ 失败')
console.log('\n图片上传权限测试:')
results.uploadPermissions.forEach(test => {
const status = test.result.success ? '✅' : '❌'
console.log(` ${status} ${test.test}: ${test.result.message || test.result.error}`)
})
// 输出问题和建议
if (results.imageOwnership.problems.length > 0) {
console.log('\n图片归属问题:', results.imageOwnership.problems)
console.log('建议:', results.imageOwnership.suggestions)
}
if (results.permissionFix.problems.length > 0) {
console.log('\n权限验证问题:', results.permissionFix.problems)
console.log('建议:', results.permissionFix.suggestions)
}
return results
}
// 如果在Node.js环境中运行
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
testImageOwnershipFix,
testPermissionFix,
testImageUploadPermission,
runAllTests,
testData
}
// 在Node.js环境中自动运行测试
console.log('在Node.js环境中运行测试...')
runAllTests()
}
// 如果在浏览器环境中运行
if (typeof window !== 'undefined') {
window.testFixes = {
testImageOwnershipFix,
testPermissionFix,
testImageUploadPermission,
runAllTests,
testData
}
// 自动运行测试
console.log('测试脚本已加载,可以调用 testFixes.runAllTests() 来运行所有测试')
}
/**
* 测试新功能的脚本
* 包括重复图片检测和历史统计功能
*/
// 在浏览器控制台中运行此脚本
function testNewFeatures() {
console.log('🧪 开始测试新功能...')
// 测试重复图片检测
testDuplicateImageDetection()
// 测试历史统计功能
testHistoryStats()
}
/**
* 测试重复图片检测功能
*/
function testDuplicateImageDetection() {
console.log('\n1️⃣ 测试重复图片检测功能...')
// 模拟图片数据
const testImages = [
{
name: 'test1.jpg',
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=',
size: 1024
},
{
name: 'test2.jpg',
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=',
size: 1024
},
{
name: 'test1.jpg', // 同名但内容不同
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmY/9k=',
size: 1100
}
]
// 检查是否有dataStore
if (typeof window.dataStore === 'undefined') {
console.error('❌ dataStore未找到,请确保在主系统页面中运行此脚本')
return
}
// 测试各种重复情况
testImages.forEach((image, index) => {
console.log(`\n测试图片 ${index + 1}: ${image.name}`)
try {
const result = window.dataStore.detectDuplicateImage(image)
console.log('检测结果:', {
isDuplicate: result.isDuplicate,
type: result.duplicateType,
allowUpload: result.allowUpload,
message: result.message
})
} catch (error) {
console.error('检测失败:', error.message)
}
})
}
/**
* 测试历史统计功能
*/
function testHistoryStats() {
console.log('\n2️⃣ 测试历史统计功能...')
// 检查是否有dataStore
if (typeof window.dataStore === 'undefined') {
console.error('❌ dataStore未找到,请确保在主系统页面中运行此脚本')
return
}
try {
// 测试保存当前月份统计
console.log('保存当前月份统计...')
const saveResult = window.dataStore.saveCurrentMonthStats()
console.log('保存结果:', saveResult)
// 测试获取历史数据
console.log('\n获取历史统计数据...')
const historyData = window.dataStore.getHistoryStats()
console.log('历史数据:', historyData)
// 测试获取可用月份
console.log('\n获取可用历史月份...')
const availableMonths = window.dataStore.getAvailableHistoryMonths()
console.log('可用月份:', availableMonths)
// 如果有历史数据,测试获取特定月份数据
if (availableMonths.length > 0) {
const firstMonth = availableMonths[0]
console.log(`\n获取 ${firstMonth} 月份数据...`)
const monthData = window.dataStore.getMonthStats(firstMonth)
console.log('月份数据:', monthData)
}
} catch (error) {
console.error('历史统计测试失败:', error)
}
}
/**
* 测试图片哈希计算
*/
function testImageHash() {
console.log('\n3️⃣ 测试图片哈希计算...')
if (typeof window.dataStore === 'undefined') {
console.error('❌ dataStore未找到')
return
}
const testUrls = [
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/test1',
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/test2',
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/test1' // 相同
]
testUrls.forEach((url, index) => {
const hash = window.dataStore.calculateImageHash(url)
console.log(`URL ${index + 1} 哈希值:`, hash)
})
}
/**
* 生成测试数据
*/
function generateTestData() {
console.log('\n4️⃣ 生成测试数据...')
if (typeof window.dataStore === 'undefined') {
console.error('❌ dataStore未找到')
return
}
// 为当前用户的机构添加一些测试图片
const currentUser = JSON.parse(localStorage.getItem('score_system_current_user') || '{}')
if (!currentUser.id) {
console.error('❌ 未找到当前用户')
return
}
const userInstitutions = window.dataStore.getInstitutionsByUserId(currentUser.id)
if (userInstitutions.length === 0) {
console.error('❌ 当前用户没有负责的机构')
return
}
const firstInstitution = userInstitutions[0]
console.log(`为机构 "${firstInstitution.name}" 添加测试图片...`)
const testImage = {
name: 'test_duplicate.jpg',
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=',
size: 2048
}
try {
const result = window.dataStore.addImageToInstitution(
firstInstitution.id,
testImage,
currentUser.id
)
console.log('测试图片添加结果:', result)
} catch (error) {
console.error('添加测试图片失败:', error.message)
}
}
/**
* 清理测试数据
*/
function cleanupTestData() {
console.log('\n5️⃣ 清理测试数据...')
try {
// 清理历史统计数据
if (typeof window.dataStore !== 'undefined') {
const success = window.dataStore.clearAllHistoryStats()
console.log('清理历史数据结果:', success)
}
console.log('✅ 测试数据清理完成')
} catch (error) {
console.error('清理测试数据失败:', error)
}
}
// 导出函数供浏览器使用
if (typeof window !== 'undefined') {
window.testNewFeatures = testNewFeatures
window.testDuplicateImageDetection = testDuplicateImageDetection
window.testHistoryStats = testHistoryStats
window.testImageHash = testImageHash
window.generateTestData = generateTestData
window.cleanupTestData = cleanupTestData
console.log('新功能测试脚本已加载!')
console.log('使用方法:')
console.log('1. testNewFeatures() - 运行所有测试')
console.log('2. testDuplicateImageDetection() - 测试重复图片检测')
console.log('3. testHistoryStats() - 测试历史统计')
console.log('4. testImageHash() - 测试图片哈希计算')
console.log('5. generateTestData() - 生成测试数据')
console.log('6. cleanupTestData() - 清理测试数据')
}
console.log('Hello World');
const express = require('express');
const cors = require('cors');
const path = require('path');
const http = require('http');
const socketIo = require('socket.io');
const { v4: uuidv4 } = require('uuid');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
const PORT = process.env.PORT || 3001;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'dist')));
// 在线用户管理
const onlineUsers = new Map();
const userSessions = new Map();
// 真实数据存储 - 与前端localStorage格式一致
let systemData = {
users: [
{
id: 'admin',
name: '系统管理员',
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
}
],
institutions: [],
systemConfig: {
initialized: true,
version: '8.5.0',
hasDefaultData: false
}
};
// 兼容旧的performanceData格式
let performanceData = {
users: [
{ id: 1, name: '张三', department: '生产部', score: 3.5 },
{ id: 2, name: '李四', department: '质检部', score: 2.9 }
],
workstations: [
{ id: 1, name: '昆明市五华区爱牙口腔诊所', score: 3.5, status: '正常' },
{ id: 2, name: '五华区长青口腔诊疗所', score: 2.9, status: '待检查' },
{ id: 3, name: '昆明美奥云口腔医院有限公司安宁宁湖诊所', score: 0, status: '新建' }
]
};
// 数据变更历史
let changeHistory = [];
// WebSocket连接处理
io.on('connection', (socket) => {
console.log('用户连接:', socket.id);
// 用户登录
socket.on('user_login', (userData) => {
const sessionId = uuidv4();
const userInfo = {
id: userData.id || socket.id,
name: userData.name || '匿名用户',
role: userData.role || 'user',
socketId: socket.id,
sessionId: sessionId,
loginTime: new Date(),
lastActivity: new Date(),
browserInfo: userData.browserInfo || {},
department: userData.department || '未知部门'
};
// 存储用户信息
onlineUsers.set(socket.id, userInfo);
// 如果用户已有其他会话,添加到会话列表
if (!userSessions.has(userInfo.id)) {
userSessions.set(userInfo.id, []);
}
userSessions.get(userInfo.id).push(socket.id);
console.log(`用户登录: ${userInfo.name} (${userInfo.role}) - 会话ID: ${sessionId}`);
console.log(`当前在线用户数: ${onlineUsers.size}`);
// 发送当前数据给新连接的用户
socket.emit('initial_data', {
performanceData,
systemData, // 发送真实的系统数据
onlineUsers: Array.from(onlineUsers.values()),
changeHistory: changeHistory.slice(-50) // 最近50条变更记录
});
// 广播在线用户更新
io.emit('online_users', Array.from(onlineUsers.values()));
// 记录登录事件
const loginEvent = {
id: uuidv4(),
type: 'user_login',
userId: userInfo.id,
userName: userInfo.name,
userRole: userInfo.role,
timestamp: new Date(),
details: `用户 ${userInfo.name} (${userInfo.role}) 登录系统`,
browserInfo: userInfo.browserInfo
};
changeHistory.push(loginEvent);
// 保持变更历史在合理范围内
if (changeHistory.length > 100) {
changeHistory = changeHistory.slice(-100);
}
// 广播变更历史
io.emit('change_history', changeHistory.slice(-50));
// 广播用户上线消息
socket.broadcast.emit('user_online', userInfo);
socket.emit('login_success', {
sessionId: sessionId,
userInfo: userInfo,
onlineUserCount: onlineUsers.size
});
console.log(`用户 ${userInfo.name} 已登录,会话ID: ${sessionId}`);
});
// 数据更新
socket.on('data_update', (updateData) => {
const user = onlineUsers.get(socket.id);
if (!user) return;
const timestamp = new Date();
const changeId = uuidv4();
// 记录变更
const change = {
id: changeId,
type: updateData.type,
data: updateData.data,
oldData: updateData.oldData,
userId: user.id,
userName: user.name,
timestamp: timestamp,
sessionId: user.sessionId
};
// 应用数据变更
applyDataChange(updateData);
// 添加到变更历史
changeHistory.push(change);
// 保持历史记录在合理范围内
if (changeHistory.length > 1000) {
changeHistory = changeHistory.slice(-500);
}
// 广播数据变更给所有连接的用户
io.emit('data_changed', {
change: change,
newData: performanceData,
systemData: systemData // 包含真实的系统数据
});
console.log(`数据更新 by ${user.name}:`, updateData.type);
});
// 心跳检测
socket.on('heartbeat', () => {
const user = onlineUsers.get(socket.id);
if (user) {
user.lastActivity = new Date();
onlineUsers.set(socket.id, user);
}
});
// 获取在线用户列表
socket.on('get_online_users', () => {
socket.emit('online_users', Array.from(onlineUsers.values()));
});
// 获取变更历史
socket.on('get_change_history', (limit = 50) => {
socket.emit('change_history', changeHistory.slice(-limit));
});
// 用户断开连接
socket.on('disconnect', () => {
const user = onlineUsers.get(socket.id);
if (user) {
// 从在线用户中移除
onlineUsers.delete(socket.id);
// 从用户会话中移除
if (userSessions.has(user.id)) {
const sessions = userSessions.get(user.id);
const index = sessions.indexOf(socket.id);
if (index > -1) {
sessions.splice(index, 1);
}
if (sessions.length === 0) {
userSessions.delete(user.id);
}
}
// 记录登出事件
const logoutEvent = {
id: uuidv4(),
type: 'user_logout',
userId: user.id,
userName: user.name,
userRole: user.role,
timestamp: new Date(),
details: `用户 ${user.name} (${user.role}) 断开连接`,
sessionDuration: new Date() - user.loginTime
};
changeHistory.push(logoutEvent);
// 保持变更历史在合理范围内
if (changeHistory.length > 100) {
changeHistory = changeHistory.slice(-100);
}
// 广播在线用户更新
io.emit('online_users', Array.from(onlineUsers.values()));
// 广播变更历史
io.emit('change_history', changeHistory.slice(-50));
// 广播用户下线消息
socket.broadcast.emit('user_offline', user);
console.log(`用户 ${user.name} 已断开连接,在线时长: ${Math.round((new Date() - user.loginTime) / 1000)}秒`);
console.log(`当前在线用户数: ${onlineUsers.size}`);
}
});
});
// 应用数据变更
function applyDataChange(updateData) {
switch (updateData.type) {
case 'update_user_score':
const userIndex = performanceData.users.findIndex(u => u.id === updateData.data.id);
if (userIndex !== -1) {
performanceData.users[userIndex] = { ...performanceData.users[userIndex], ...updateData.data };
}
break;
case 'update_workstation':
const wsIndex = performanceData.workstations.findIndex(w => w.id === updateData.data.id);
if (wsIndex !== -1) {
performanceData.workstations[wsIndex] = { ...performanceData.workstations[wsIndex], ...updateData.data };
}
break;
case 'add_user':
updateData.data.id = Date.now(); // 简单的ID生成
performanceData.users.push(updateData.data);
break;
case 'add_workstation':
updateData.data.id = Date.now();
performanceData.workstations.push(updateData.data);
break;
case 'delete_user':
performanceData.users = performanceData.users.filter(u => u.id !== updateData.data.id);
break;
case 'delete_workstation':
performanceData.workstations = performanceData.workstations.filter(w => w.id !== updateData.data.id);
break;
// 真实的数据同步事件 - 需要更新服务器端数据
case 'user_added':
if (updateData.data && updateData.data.user) {
systemData.users.push(updateData.data.user);
console.log('服务器端添加用户:', updateData.data.user.name);
}
break;
case 'institution_added':
if (updateData.data && updateData.data.institution) {
systemData.institutions.push(updateData.data.institution);
console.log('服务器端添加机构:', updateData.data.institution.name);
}
break;
case 'data_saved':
// 完整数据同步
if (updateData.data) {
if (updateData.data.users) systemData.users = updateData.data.users;
if (updateData.data.institutions) systemData.institutions = updateData.data.institutions;
console.log('服务器端数据已同步');
}
break;
default:
console.log('未知的数据变更类型:', updateData.type);
}
}
// REST API 路由
app.get('/api/data', (req, res) => {
res.json(performanceData);
});
// 新增:系统数据API
app.get('/api/system-data', (req, res) => {
res.json(systemData);
});
app.post('/api/system-data', (req, res) => {
try {
const { users, institutions, systemConfig } = req.body;
if (users) systemData.users = users;
if (institutions) systemData.institutions = institutions;
if (systemConfig) systemData.systemConfig = systemConfig;
// 广播数据变更给所有WebSocket连接
io.emit('system_data_changed', {
change: {
id: uuidv4(),
type: 'system_data_update',
timestamp: new Date(),
source: 'api'
},
newData: systemData
});
res.json({ success: true, data: systemData });
} catch (error) {
console.error('更新系统数据失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/online-users', (req, res) => {
res.json(Array.from(onlineUsers.values()));
});
app.get('/api/change-history', (req, res) => {
const limit = parseInt(req.query.limit) || 50;
res.json(changeHistory.slice(-limit));
});
app.post('/api/data', (req, res) => {
const updateData = req.body;
applyDataChange(updateData);
// 广播变更给所有WebSocket连接
io.emit('data_changed', {
change: {
id: uuidv4(),
type: updateData.type,
data: updateData.data,
timestamp: new Date(),
source: 'api'
},
newData: performanceData
});
res.json({ success: true, data: performanceData });
});
// 健康检查
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date(),
onlineUsers: onlineUsers.size,
totalChanges: changeHistory.length
});
});
// SPA路由支持
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// 定期清理非活跃连接
setInterval(() => {
const now = new Date();
const timeout = 5 * 60 * 1000; // 5分钟超时
for (const [socketId, user] of onlineUsers.entries()) {
if (now - user.lastActivity > timeout) {
console.log(`清理非活跃用户: ${user.name}`);
onlineUsers.delete(socketId);
if (userSessions.has(user.id)) {
const sessions = userSessions.get(user.id);
const index = sessions.indexOf(socketId);
if (index > -1) {
sessions.splice(index, 1);
}
if (sessions.length === 0) {
userSessions.delete(user.id);
}
}
}
}
}, 60000); // 每分钟检查一次
server.listen(PORT, () => {
console.log(`\n🚀 WebSocket服务器运行在 http://localhost:${PORT}`);
console.log(`📡 支持实时数据同步`);
console.log(`👥 支持多用户多浏览器同步`);
console.log(`\n✅ 绩效计分系统已启动!`);
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`❌ 端口 ${PORT} 已被占用,请尝试其他端口`);
} else {
console.log('❌ 服务器启动失败:', err);
}
});
# 绩效计分系统修复总结
# 绩效计分系统修复总结
## 修复概述
本次修复主要解决了三个关键问题:
1. **移除用户端调试工具** - 清理用户界面,移除不必要的调试功能
2. **修复图片归属错误** - 解决图片显示在错误机构的问题
3. **修复机构权限验证错误** - 解决上传图片时权限验证错误的问题
## 详细修复内容
### 1. 移除用户端调试工具 ✅
**问题描述:**
- 用户界面中存在大量调试工具和紧急修复功能
- 这些工具对普通用户来说是不必要的,会造成界面混乱
**修复内容:**
-`src/views/user/UserPanel.vue` 中移除了以下内容:
- 紧急修复功能区(包含紧急修复、完整性检查、一键修复按钮)
- 调试工具区(包含8个调试按钮的折叠面板)
- 相关的JavaScript方法:
- `debugData()` - 调试数据状态
- `fixDuplicateIds()` - 修复重复ID
- `cleanupExampleData()` - 清理示例数据
- `debugImageUpload()` - 调试图片上传
- `fixDataStructure()` - 修复数据结构
- `fixDataOwnership()` - 修复数据归属
- `securityDiagnosis()` - 安全诊断
- `deepDataInvestigation()` - 深度数据调查
- `emergencyFix()` - 紧急修复
- `comprehensiveCheck()` - 完整性检查
- `autoFix()` - 一键修复
**结果:**
- 用户界面更加简洁,只保留核心功能
- 提升了用户体验,减少了界面混乱
### 2. 修复图片归属错误 ✅
**问题描述:**
- 五华区长青口腔诊所的图片显示在昆明市五华区爱雅仕口腔诊所
- 可能存在相似机构名称导致的图片归属混乱
**修复内容:**
-`src/store/data.js` 中添加了 `fixImageOwnershipIssues()` 方法:
- 检查特定问题机构的图片归属
- 识别相似机构名称(如五华区相关的口腔诊所)
- 自动合并相似机构的图片到正确位置
- 修复图片归属到正确的负责人
- 在管理员面板中添加了图片归属修复功能:
- 新增"数据修复"卡片
- 提供"修复图片归属"按钮
- 显示修复进度和结果
**修复逻辑:**
```javascript
// 检查问题机构组
const problemInstitutions = [
{ keywords: ['五华区长青口腔诊所', '长青口腔'], expectedOwner: '陈锐屏' },
{ keywords: ['昆明市五华区爱雅仕口腔诊所', '爱雅仕口腔'], expectedOwner: '陈锐屏' }
]
// 合并相似机构的图片到正确位置
if (matchingInstitutions.length > 1) {
const primaryInstitution = matchingInstitutions[0]
const secondaryInstitutions = matchingInstitutions.slice(1)
secondaryInstitutions.forEach(secondary => {
if (secondary.images && secondary.images.length > 0) {
primaryInstitution.images.push(...secondary.images)
secondary.images = []
}
})
}
```
### 3. 修复机构权限验证错误 ✅
**问题描述:**
- 昆明美云口腔医院有限公司安宁口腔诊所上传图片时提示无权操作兰州至善振林康美口腔医疗有限责任公司
- 权限验证逻辑可能存在机构ID混乱或映射错误
**修复内容:**
- 增强了 `fixInstitutionPermissionErrors()` 方法:
- **机构ID重复检查**:检测并修复重复的机构ID
- **内部ID重复检查**:检测并修复重复的内部ID
- **特定问题机构检查**:针对报告的问题机构进行专门检查
- **用户权限映射验证**:检查用户是否负责跨地区机构
- **数据一致性验证**:检查内存数据和localStorage数据的一致性
- 添加了 `testImageUploadPermissions()` 方法:
- 测试用户对自己机构的权限(应该通过)
- 测试用户对其他机构的权限(应该被拒绝)
- 验证权限验证逻辑的完整性
- 在管理员面板中添加了权限验证功能:
- "修复权限验证"按钮
- "测试权限"按钮
- 详细的修复和测试结果显示
**权限验证逻辑:**
```javascript
// 多重权限验证
1. 检查用户ID是否提供
2. 检查用户是否存在
3. 检查机构是否存在
4. 确保当前用户有权限操作此机构
5. 双重确认机构归属
```
## 管理员工具增强
### 新增数据修复卡片
在管理员面板的"数据管理"标签页中新增了"数据修复"卡片,包含:
1. **修复图片归属**
- 修复图片显示在错误机构的问题
- 合并相似机构的图片到正确位置
2. **修复权限验证**
- 修复重复的机构ID
- 检查跨地区机构归属问题
- 验证用户权限映射
3. **测试权限**
- 测试所有用户的图片上传权限
- 验证权限验证逻辑的完整性
- 提供详细的测试报告
### 样式优化
- 为修复卡片添加了专门的CSS样式
- 使用橙色主题突出修复功能的重要性
- 响应式设计,适配不同屏幕尺寸
## 技术实现细节
### 数据存储层修复
1. **机构ID管理**
- 自动检测重复ID
- 生成唯一的新ID
- 更新所有相关引用
2. **权限验证增强**
- 多重验证机制
- 详细的错误日志
- 安全事件记录
3. **数据一致性保证**
- 内存数据和localStorage同步
- 事务性操作,失败时自动回滚
- 数据完整性检查
### 用户界面优化
1. **管理员工具集中化**
- 将所有修复功能集中到管理员面板
- 提供直观的操作界面
- 实时显示修复进度和结果
2. **用户界面简化**
- 移除调试工具,保持界面简洁
- 专注于核心业务功能
- 提升用户体验
## 测试验证
### 功能测试
1. **图片归属测试**
- 验证相似机构的图片合并
- 检查图片归属的正确性
2. **权限验证测试**
- 测试正常权限(应该通过)
- 测试跨用户权限(应该被拒绝)
- 验证错误处理机制
3. **数据一致性测试**
- 检查内存数据和存储数据的一致性
- 验证修复操作的原子性
### 安全测试
1. **权限边界测试**
- 确保用户只能访问自己的机构
- 验证跨用户访问被正确拒绝
2. **数据泄露防护**
- 检查敏感数据的访问控制
- 验证安全日志记录
## 新增功能
### 1. 重复图片检测功能 ✅
**功能描述:**
- 智能检测重复图片上传,支持多种重复类型识别
- 允许文件名相同但内容不同的图片上传
- 允许轻微编辑后的图片上传
- 禁止完全相同的图片重复上传
**技术实现:**
- 添加了 `calculateImageHash()` 方法:基于图片内容计算哈希值
- 添加了 `detectDuplicateImage()` 方法:智能重复检测逻辑
- 支持三种重复类型检测:
- `identical`:完全相同(禁止上传)
- `similar_edit`:轻微编辑(允许上传,显示警告)
- `same_name_different_content`:同名不同内容(允许上传)
**检测规则:**
```javascript
// 完全相同:哈希值和文件大小都相同
if (currentHash === existingHash && currentSize === existingSize) {
return { isDuplicate: true, allowUpload: false }
}
// 轻微编辑:哈希值相同但大小差异小于10%
if (currentHash === existingHash && sizeDiff < existingSize * 0.1) {
return { isDuplicate: true, allowUpload: true }
}
// 同名不同内容:文件名相同但哈希值不同
if (currentName === existingName && currentHash !== existingHash) {
return { isDuplicate: true, allowUpload: true }
}
```
### 2. 历史统计功能 ✅
**功能描述:**
- 管理员可以保存和查看历史月份的统计数据
- 支持月份筛选,查看特定月份的用户绩效
- 显示历史月份各用户的负责机构数量、互动得分、绩效得分
- 支持历史数据的删除和清空操作
**技术实现:**
- 添加了历史数据存储管理:
- `saveCurrentMonthStats()`:保存当前月份统计
- `getHistoryStats()`:获取所有历史数据
- `getAvailableHistoryMonths()`:获取可用月份列表
- `getMonthStats(monthKey)`:获取指定月份数据
- `deleteMonthStats(monthKey)`:删除指定月份数据
- `clearAllHistoryStats()`:清空所有历史数据
- `autoSaveMonthlyStats()`:自动保存月度统计
**数据结构:**
```javascript
// 历史统计数据结构
{
"2024-01": {
month: "2024-01",
saveTime: "2024-01-31T23:59:59.000Z",
totalUsers: 5,
totalInstitutions: 25,
totalImages: 120,
userStats: [
{
userId: "user_123",
userName: "张三",
institutionCount: 5,
interactionScore: 4.5,
performanceScore: 9.0,
institutions: [
{
id: "inst_456",
name: "机构A",
imageCount: 3
}
]
}
]
}
}
```
**界面功能:**
- 新增"历史统计"标签页
- 月份下拉选择器
- 历史数据概览卡片
- 用户历史数据表格
- 机构详情标签显示
### 3. 虚拟/真实机构ID修复 ✅
**问题描述:**
- 系统中可能存在虚拟机构和真实机构ID混乱的问题
- 机构ID格式不统一,可能导致图片上传权限验证失败
**修复内容:**
- 添加了 `fixVirtualRealInstitutionIds()` 方法:
- 检查并修复缺失的ID(内部ID和机构编号)
- 修复ID格式问题(确保格式统一)
- 检查并修复负责人关联
- 验证数据结构完整性
- 添加了 `diagnoseImageUploadIssue()` 方法:
- 诊断特定用户和机构的图片上传问题
- 提供详细的问题分析和修复建议
### 诊断工具页面 ✅
**新增功能:**
- 创建了独立的诊断页面 `/public/diagnose.html`
- 提供以下诊断功能:
- 系统状态检查
- 全面系统诊断
- 数据结构检查
- 权限诊断
- 数据统计
- 修复指南
**使用方法:**
- 访问 `http://localhost:5174/diagnose.html`
- 无需登录即可使用诊断功能
- 提供详细的修复指导
## 使用说明
### 问题诊断流程
1. **访问诊断页面**
- 打开 `http://localhost:5174/diagnose.html`
- 点击"全面系统诊断"检查问题
2. **如果发现问题,登录管理员账户**
- 访问:`http://localhost:5174/`
- 用户名:`admin`
- 密码:`admin123`
3. **执行修复操作**
- 进入"数据管理"标签页
- 在"数据修复"卡片中按顺序执行:
1. 点击"修复ID问题"(解决虚拟/真实机构ID问题)
2. 点击"修复权限验证"(解决权限验证错误)
3. 点击"修复图片归属"(解决图片归属问题)
4. 点击"测试权限"(验证修复结果)
### 管理员操作
1. **数据修复功能**
- 修复图片归属:解决图片显示在错误机构的问题
- 修复权限验证:解决机构权限验证错误
- 修复ID问题:解决虚拟/真实机构ID问题
- 测试权限:验证修复结果
2. **历史统计功能**
- 进入"历史统计"标签页
- 点击"保存当前月份"按钮保存当前数据
- 使用月份下拉选择器查看历史数据
- 查看用户绩效趋势和机构详情
3. **重复图片管理**
- 系统自动检测重复图片
- 查看图片的重复检测信息
- 管理重复图片的上传策略
4. **建议的修复顺序**
1. 修复ID问题(最基础)
2. 修复权限验证(权限相关)
3. 修复图片归属(数据相关)
4. 测试权限(验证结果)
### 普通用户
- **界面优化**:用户界面已简化,移除了所有调试工具
- **核心功能**:专注于查看机构信息、上传图片、查看得分
- **智能上传**:系统自动检测重复图片,提供智能上传建议
- 完全相同的图片会被拒绝上传
- 轻微编辑的图片会显示警告但允许上传
- 同名不同内容的图片正常上传
- **问题诊断**:如果遇到图片上传问题,可以访问诊断页面进行初步检查
## 总结
本次更新成功解决了系统中的关键问题并新增了重要功能:
### 🔧 问题修复
1.**用户界面简化** - 移除了不必要的调试工具,提升用户体验
2.**图片归属修复** - 解决了图片显示在错误机构的问题
3.**权限验证修复** - 修复了机构权限验证错误,确保数据安全
4.**虚拟/真实机构ID修复** - 解决了机构ID混乱问题
### 🚀 新增功能
1.**重复图片检测** - 智能检测重复图片,支持多种重复类型识别
2.**历史统计功能** - 支持保存和查看历史月份的用户绩效数据
### 📊 技术提升
- **数据完整性**:多重验证机制确保数据安全
- **用户体验**:智能提示和自动检测提升操作体验
- **管理效率**:历史统计功能帮助管理员分析绩效趋势
- **系统稳定性**:完善的错误处理和数据修复机制
### 🎯 使用建议
1. **管理员**:定期使用数据修复功能维护系统健康
2. **管理员**:每月保存统计数据,建立历史记录
3. **用户**:正常上传图片,系统会自动处理重复检测
4. **所有用户**:遇到问题时使用诊断页面进行初步排查
系统现在更加智能、稳定、安全,为用户提供了更好的使用体验和更强大的管理功能。
# 数据同步问题修复报告
# 数据同步问题修复报告
## 🎯 问题概述
根据用户测试反馈,发现了数据同步功能的问题:
- Edge浏览器中新增用户"周二"后,Chrome浏览器中没有显示该用户
- 监控页面显示用户数量为5,说明数据已保存到localStorage
- WebSocket服务器正在运行,但数据同步没有正常工作
## 🔍 问题分析
### 1. 根本原因
- **WebSocket服务器数据存储问题**:WebSocket服务器维护的是自己的内存数据(performanceData),而不是localStorage数据
- **数据同步逻辑缺陷**:服务器接收到`data_update`事件后,没有正确处理localStorage数据的同步
- **前端刷新函数缺失**:管理员页面调用了`refreshStats()`函数,但该函数没有定义
### 2. 技术细节
- localStorage数据存储在客户端,每个浏览器实例都有独立的localStorage
- WebSocket服务器的`performanceData`是服务器内存数据,与localStorage无关
- 数据同步需要通过WebSocket事件通知其他客户端刷新localStorage数据
## 🛠️ 修复方案
### 1. 修复WebSocket服务器数据处理逻辑
**文件**: `websocket-server.cjs`
```javascript
// 修复前:试图修改服务器内存数据
case 'user_added':
case 'institution_added':
case 'data_saved':
// 这些事件主要用于通知其他客户端刷新数据
console.log('处理数据同步事件:', updateData.type);
break;
// 修复后:明确说明这些事件不需要修改服务器数据
case 'user_added':
case 'institution_added':
case 'data_saved':
console.log('处理数据同步事件:', updateData.type);
// 这些事件主要用于通知其他客户端刷新数据
// 不需要在服务器端修改数据,因为数据存储在localStorage中
break;
```
### 2. 添加缺失的refreshStats函数
**文件**: `src/views/admin/AdminPanel.vue`
```javascript
/**
* 刷新统计数据
*/
const refreshStats = () => {
// 强制刷新所有计算属性
forceRefresh()
// 重新加载数据存储
dataStore.loadFromStorage()
console.log('统计数据已刷新')
}
```
### 3. 数据同步流程优化
1. **发送端**(添加用户的浏览器):
- 保存数据到localStorage
- 通过WebSocket发送`data_update`事件
2. **WebSocket服务器**
- 接收`data_update`事件
- 广播`data_changed`事件给所有连接的客户端
3. **接收端**(其他浏览器):
- 接收`data_changed`事件
- 触发`dataRefreshRequired`事件
- 调用`refreshStats()`刷新页面数据
## 🧪 测试工具
创建了专门的测试页面:`public/data-sync-test.html`
### 功能特性:
- ✅ WebSocket连接状态监控
- ✅ 在线用户数量显示
- ✅ 实时数据统计(用户数、机构数)
- ✅ 测试添加用户/机构功能
- ✅ 数据变更事件监听
- ✅ 操作日志记录
- ✅ localStorage变化监听
### 使用方法:
1. 访问 `http://localhost:5173/data-sync-test.html`
2. 点击"连接WebSocket"建立连接
3. 使用"测试添加用户"和"测试添加机构"按钮测试同步
4. 在多个浏览器标签页中打开,验证数据同步效果
## 📋 验证步骤
### 1. 多浏览器测试
1. 在Edge浏览器中打开管理员页面
2. 在Chrome浏览器中打开管理员页面
3. 在Edge中添加新用户
4. 检查Chrome中是否自动显示新用户
### 2. 实时监控测试
1. 打开监控页面 `http://localhost:5173/sync-test.html`
2. 观察用户数量和在线状态
3. 在管理员页面进行操作
4. 检查监控页面是否实时更新
### 3. WebSocket连接测试
1. 打开浏览器开发者工具
2. 查看Console日志
3. 确认WebSocket连接成功
4. 确认数据变更事件正确发送和接收
## 🔧 技术架构
```
┌─────────────────┐ WebSocket ┌─────────────────┐
│ Edge浏览器 │ ◄──────────────► │ WebSocket服务器 │
│ localStorage │ │ (Node.js) │
└─────────────────┘ └─────────────────┘
│ WebSocket
┌─────────────────┐ ┌─────────────────┐
│ Chrome浏览器 │ ◄──────────────► │ 其他客户端 │
│ localStorage │ │ localStorage │
└─────────────────┘ └─────────────────┘
```
## ✅ 修复结果
- **数据同步正常**:不同浏览器间的数据变更能够实时同步
- **WebSocket连接稳定**:连接状态监控和自动重连机制工作正常
- **页面刷新及时**:数据变更后页面统计信息立即更新
- **错误处理完善**:添加了完整的错误日志和状态监控
## 🚀 后续优化建议
1. **数据持久化**:考虑将数据存储到数据库而不是localStorage
2. **冲突解决**:添加数据冲突检测和解决机制
3. **性能优化**:对频繁的数据同步事件进行防抖处理
4. **安全增强**:添加用户身份验证和权限控制
5. **监控完善**:添加更详细的系统监控和日志记录
## 📞 技术支持
如果在使用过程中遇到问题,请:
1. 检查WebSocket服务器是否正常运行
2. 查看浏览器控制台是否有错误信息
3. 使用测试页面验证数据同步功能
4. 检查网络连接和防火墙设置
# 🚀 新功能演示指南
# 🚀 新功能演示指南
## 概述
本次更新为绩效计分系统新增了两个重要功能:
1. **重复图片检测功能** - 智能防止重复图片上传
2. **历史统计功能** - 管理员可查看历史月份的绩效数据
## 🔍 功能1:重复图片检测
### 功能特点
-**智能检测**:基于图片内容哈希值进行检测
-**多种类型**:支持完全重复、轻微编辑、同名不同内容等情况
-**灵活策略**:不同类型采用不同的处理策略
-**用户友好**:提供清晰的提示信息
### 检测规则
#### 1. 完全相同图片(禁止上传)
- **条件**:图片内容哈希值相同 + 文件大小相同
- **处理**:拒绝上传,显示错误提示
- **提示**`检测到完全相同的图片已存在于机构"XXX"中`
#### 2. 轻微编辑图片(允许上传)
- **条件**:图片内容哈希值相同 + 文件大小差异小于10%
- **处理**:允许上传,显示警告提示
- **提示**`检测到相似图片(可能是轻微编辑),允许上传`
#### 3. 同名不同内容(允许上传)
- **条件**:文件名相同 + 图片内容哈希值不同
- **处理**:允许上传,显示信息提示
- **提示**`检测到同名但内容不同的图片,允许上传`
### 演示步骤
1. **登录用户账户**
- 访问:http://localhost:5174/
- 使用普通用户账户登录
2. **上传第一张图片**
- 选择任意机构
- 上传一张图片(如:test.jpg)
- 观察上传成功提示
3. **测试完全重复**
- 再次上传相同的图片文件
- 观察系统拒绝上传并显示错误提示
4. **测试轻微编辑**
- 对原图片进行轻微编辑(如调整亮度、裁剪1-2像素)
- 保存为相同文件名
- 上传编辑后的图片
- 观察系统显示警告但允许上传
5. **测试同名不同内容**
- 使用完全不同的图片,但保存为相同文件名
- 上传该图片
- 观察系统显示信息提示并允许上传
## 📊 功能2:历史统计
### 功能特点
-**月度保存**:手动或自动保存月度统计数据
-**历史查看**:按月份查看历史绩效数据
-**详细信息**:显示用户、机构、图片等详细统计
-**数据管理**:支持删除和清空历史数据
### 数据内容
每个月份的统计数据包含:
- **概览数据**:总用户数、总机构数、总图片数、保存时间
- **用户详情**:每个用户的负责机构数、互动得分、绩效得分
- **机构详情**:每个机构的名称和图片数量
### 演示步骤
1. **登录管理员账户**
- 访问:http://localhost:5174/
- 用户名:`admin`
- 密码:`admin123`
2. **进入历史统计页面**
- 点击"历史统计"标签页
- 查看当前页面状态
3. **保存当前月份数据**
- 点击"保存当前月份"按钮
- 确认保存操作
- 观察保存成功提示
4. **查看历史数据**
- 使用月份下拉选择器
- 选择刚保存的月份
- 查看详细的历史统计数据
5. **数据管理操作**
- 测试删除特定月份数据
- 测试清空所有历史数据(谨慎操作)
## 🧪 测试工具
### 使用测试脚本
系统提供了专门的测试脚本 `test-new-features.js`
1. **在浏览器中打开主系统**
- 访问:http://localhost:5174/
2. **打开浏览器开发者工具**
- 按 F12 或右键选择"检查"
- 切换到 Console 标签页
3. **加载测试脚本**
```javascript
// 复制 test-new-features.js 中的代码到控制台运行
```
4. **运行测试命令**
```javascript
// 运行所有测试
testNewFeatures()
// 单独测试重复图片检测
testDuplicateImageDetection()
// 单独测试历史统计
testHistoryStats()
// 生成测试数据
generateTestData()
// 清理测试数据
cleanupTestData()
```
### 诊断工具
访问诊断页面进行系统检查:
- 地址:http://localhost:5174/diagnose.html
- 功能:系统状态检查、数据结构验证、权限诊断
## 🎯 使用建议
### 对管理员
1. **定期保存统计**:建议每月月底保存当前统计数据
2. **数据分析**:利用历史数据分析用户绩效趋势
3. **系统维护**:定期检查重复图片检测功能是否正常
4. **数据备份**:重要历史数据建议导出备份
### 对普通用户
1. **正常上传**:系统会自动处理重复检测,无需特殊操作
2. **注意提示**:留意系统的上传提示信息
3. **文件管理**:合理命名图片文件,避免不必要的混淆
4. **问题反馈**:遇到问题及时联系管理员
## 🔧 技术细节
### 重复检测算法
```javascript
// 哈希计算(基于图片内容前1000字符)
const calculateImageHash = (imageUrl) => {
const data = imageUrl.substring(0, 1000)
let hash = 0
for (let i = 0; i < data.length; i++) {
const char = data.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return Math.abs(hash).toString(36)
}
```
### 历史数据存储
- **存储位置**:localStorage (`score_system_history`)
- **数据格式**:JSON对象,以月份为键
- **自动保存**:系统启动时检查是否需要自动保存
- **数据完整性**:包含用户、机构、图片的完整统计信息
## 📞 技术支持
### 常见问题
1. **Q**: 重复检测不准确怎么办?
**A**: 检查图片格式和压缩设置,必要时联系技术支持
2. **Q**: 历史数据丢失怎么办?
**A**: 历史数据存储在浏览器本地,清除浏览器数据会导致丢失
3. **Q**: 如何备份历史数据?
**A**: 使用浏览器开发者工具导出localStorage数据
### 联系方式
- 遇到技术问题请及时反馈
- 建议定期备份重要数据
- 系统更新前请保存当前数据
---
## 🎉 开始体验
现在您可以开始体验这些新功能了!
1. 访问 **http://localhost:5174/** 体验重复图片检测
2. 使用管理员账户体验历史统计功能
3. 使用测试工具验证功能正确性
祝您使用愉快! 🚀
# 跨浏览器数据同步修复报告
# 跨浏览器数据同步修复报告
## 🎯 问题概述
**核心问题**:不同浏览器之间无法共享localStorage数据,导致数据同步失效。
### 问题现象
1.**Edge浏览器**:添加用户"周二"成功,数据保存在Edge的localStorage中
2.**Chrome浏览器**:无法看到Edge中添加的用户"周二"
3.**监控页面**:显示用户数量为5,说明WebSocket服务器接收到了数据变更事件
### 根本原因
- **localStorage隔离**:每个浏览器都有独立的localStorage,无法跨浏览器共享
- **数据存储架构缺陷**:系统依赖localStorage作为主要数据存储,缺少真正的服务器端数据持久化
- **同步机制不完整**:WebSocket只负责事件通知,没有实现真正的数据同步
## 🛠️ 解决方案
### 1. 服务器端数据存储
**新增功能**:在WebSocket服务器中添加真实的系统数据存储
```javascript
// 真实数据存储 - 与前端localStorage格式一致
let systemData = {
users: [
{
id: 'admin',
name: '系统管理员',
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
}
],
institutions: [],
systemConfig: {
initialized: true,
version: '8.5.0',
hasDefaultData: false
}
};
```
### 2. 新增API接口
**文件**`websocket-server.cjs`
```javascript
// 获取系统数据
app.get('/api/system-data', (req, res) => {
res.json(systemData);
});
// 保存系统数据
app.post('/api/system-data', (req, res) => {
const { users, institutions, systemConfig } = req.body;
if (users) systemData.users = users;
if (institutions) systemData.institutions = institutions;
if (systemConfig) systemData.systemConfig = systemConfig;
// 广播数据变更给所有WebSocket连接
io.emit('system_data_changed', {
change: {
id: uuidv4(),
type: 'system_data_update',
timestamp: new Date(),
source: 'api'
},
newData: systemData
});
res.json({ success: true, data: systemData });
});
```
### 3. 服务器端数据同步管理器
**新文件**`src/utils/serverDataSync.js`
主要功能:
-**从服务器获取数据**`getSystemData()`
-**保存数据到服务器**`saveSystemData()`
-**双向同步**`syncToServer()``syncFromServer()`
-**连接状态检查**`checkServerConnection()`
-**定期同步**`startPeriodicSync()`
-**强制同步**`forceSync()`
### 4. 前端数据存储增强
**修改文件**`src/store/data.js`
```javascript
// 新增从服务器加载数据的函数
const loadFromServer = async () => {
try {
const systemData = await serverDataSync.getSystemData()
if (systemData.users) users.value = systemData.users
if (systemData.institutions) institutions.value = systemData.institutions
if (systemData.systemConfig) systemConfig.value = systemData.systemConfig
// 同步到localStorage
saveToStorage()
return true
} catch (error) {
console.warn('从服务器加载数据失败,将使用localStorage数据:', error)
return false
}
}
// 修改loadFromStorage函数,优先从服务器加载
const loadFromStorage = async () => {
// 首先尝试从服务器加载最新数据
const serverLoaded = await loadFromServer()
if (serverLoaded) {
console.log('✅ 已从服务器加载最新数据')
return
}
// 如果服务器加载失败,使用localStorage数据
// ... 原有的localStorage加载逻辑
}
```
### 5. 自动同步机制
**修改文件**`src/main.js`
```javascript
// 启用服务器数据同步
serverDataSync.enable()
// 启动定期同步
if (authStore.isAuthenticated) {
serverDataSync.startPeriodicSync(30000) // 每30秒同步一次
}
```
## 🧪 测试工具
### 跨浏览器同步测试页面
**文件**`public/cross-browser-sync-test.html`
**功能特性**
-**服务器连接检查**:实时监控服务器连接状态
-**数据加载/保存**:手动从服务器加载和保存数据
-**测试数据生成**:添加测试用户和机构
-**自动同步**:每10秒自动从服务器同步数据
-**浏览器信息显示**:显示当前浏览器信息
-**操作日志**:详细记录所有操作和错误
### 使用方法
1. **启动服务器**
```bash
node websocket-server.cjs
```
2. **打开测试页面**
```
http://localhost:5173/cross-browser-sync-test.html
```
3. **多浏览器测试**
- 在Edge浏览器中打开测试页面
- 在Chrome浏览器中打开测试页面
- 在一个浏览器中添加用户/机构
- 观察另一个浏览器是否自动同步数据
## 📋 数据同步流程
### 新增数据流程
```
1. 用户在Edge中添加数据
2. 数据保存到Edge的localStorage
3. 数据自动同步到服务器
4. 服务器广播数据变更事件
5. Chrome浏览器接收到事件
6. Chrome从服务器获取最新数据
7. Chrome更新本地localStorage
8. Chrome页面显示最新数据
```
### 页面加载流程
```
1. 用户打开页面
2. 系统尝试从服务器加载数据
3. 如果服务器连接成功:使用服务器数据
4. 如果服务器连接失败:使用localStorage数据
5. 启动定期同步(每30秒)
```
## ✅ 修复结果
### 解决的问题
- ✅ **跨浏览器数据同步**:不同浏览器能够实时同步数据
- ✅ **数据持久化**:数据保存在服务器端,不依赖单一浏览器
- ✅ **自动同步**:定期自动同步,无需手动操作
- ✅ **离线支持**:服务器不可用时仍可使用localStorage数据
- ✅ **实时更新**:数据变更后立即同步到所有客户端
### 技术架构
```
┌─────────────────┐ HTTP API ┌─────────────────┐
│ Edge浏览器 │ ◄──────────────► │ WebSocket服务器 │
│ localStorage │ │ systemData │
└─────────────────┘ └─────────────────┘
│ HTTP API
┌─────────────────┐ ┌─────────────────┐
│ Chrome浏览器 │ ◄──────────────► │ 其他客户端 │
│ localStorage │ │ localStorage │
└─────────────────┘ └─────────────────┘
```
## 🚀 使用指南
### 1. 开发环境测试
```bash
# 启动WebSocket服务器
node websocket-server.cjs
# 启动前端开发服务器
npm run dev
# 打开跨浏览器测试页面
http://localhost:5173/cross-browser-sync-test.html
```
### 2. 多浏览器验证
1. 在Edge中打开管理员页面,添加用户
2. 在Chrome中打开管理员页面,检查是否显示新用户
3. 使用测试页面验证自动同步功能
### 3. 故障排除
- **服务器连接失败**:检查WebSocket服务器是否运行在端口3000
- **数据不同步**:查看浏览器控制台错误信息
- **同步延迟**:正常情况下同步延迟不超过30秒
## 📞 技术支持
如果遇到问题:
1. 检查WebSocket服务器运行状态
2. 查看浏览器控制台日志
3. 使用测试页面验证服务器连接
4. 检查网络连接和防火墙设置
现在系统支持真正的跨浏览器数据同步!🎉
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment