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;
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>
/**
* 数据清理工具
* 用于清理系统中的用户和机构数据,只保留管理员
*/
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
# 绩效计分系统启动脚本
# 用于启动前端服务器和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');
This diff is collapsed. Click to expand it.
# 数据同步问题修复报告
# 数据同步问题修复报告
## 🎯 问题概述
根据用户测试反馈,发现了数据同步功能的问题:
- 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