Commit eca0a9b0 by Performance System

🔀 合并 v8.6-clean 到 master: 8.8版本发布

 主要特性:
- 图片上传归属修复
- Docker 容器化部署
- 前端路由回退支持
- 权限验证增强

🐳 Docker 部署:
- 多阶段构建优化
- Nginx SPA 配置
- 健康检查机制
- 一键部署脚本

🔧 技术改进:
- 数据自动修复
- 启动脚本升级
- 响应式更新优化
parents bcdb7b5d 728c4097
# 多阶段构建 - 构建阶段
FROM node:18-alpine AS builder
# 8.8 版本 Dockerfile - 生产镜像(多阶段构建)
# 设置工作目录
# 1) Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 只拷贝必要文件以提高缓存命中率
COPY package.json package-lock.json* ./
RUN npm ci --silent || npm install --legacy-peer-deps
# 复制源代码
# 拷贝源码并构建
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine AS production
# 2) Runtime stage (Nginx)
FROM nginx:alpine
# 安装Node.js用于运行后端服务
RUN apk add --no-cache nodejs npm
# 安装 bash 以便容器内调试(可选)
RUN apk add --no-cache bash
# 创建应用目录
WORKDIR /app
# 从构建阶段复制构建结果
# 拷贝构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制后端服务文件
COPY --from=builder /app/server.js /app/
COPY --from=builder /app/package*.json /app/
# 安装后端依赖
RUN npm ci --only=production
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 创建启动脚本
RUN echo '#!/bin/sh' > /start.sh && \
echo 'node /app/server.js &' >> /start.sh && \
echo 'nginx -g "daemon off;"' >> /start.sh && \
chmod +x /start.sh
# 拷贝 Nginx 配置(启用前端路由回退)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80 3000
EXPOSE 80
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# 健康检查(可选)
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost/ || exit 1
# 启动服务
CMD ["/start.sh"]
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
# 绩效计分系统 8.8 版本 - Docker 部署说明
# 绩效计分系统 8.8 版本 - Docker 部署说明
## 🎉 部署成功!
绩效计分系统 8.8 版本已成功部署到 Docker 容器中,现在可以通过以下方式访问:
### 🌐 访问地址
- **主要访问地址**: http://localhost:3000/
- **管理员面板**: http://localhost:3000/admin
- **用户面板**: http://localhost:3000/user
### 👥 默认登录账号
- **管理员**: admin / admin123
- **张田田**: 13800138002 / 123456
- **陈锐屏**: 13800138001 / 123456
- **余芳飞**: 13800138003 / 123456
### 🐳 容器信息
- **容器名称**: scoring-system-8.8
- **镜像版本**: scoring-system:8.8
- **端口映射**: 3000:80
- **状态**: 运行中 (健康检查启用)
### 🛠️ 管理命令
#### 快速管理
- **一键管理**: 双击 `docker-manage.bat`
- **重新部署**: 双击 `docker-deploy.bat`
#### 手动命令
```bash
# 查看容器状态
docker ps
# 查看日志
docker logs scoring-system-8.8
# 重启容器
docker restart scoring-system-8.8
# 停止容器
docker stop scoring-system-8.8
# 启动容器
docker start scoring-system-8.8
# 删除容器
docker rm -f scoring-system-8.8
```
### 🔧 技术特性
- ✅ 前端路由支持 (刷新 /admin 不会 404)
- ✅ 图片上传权限修复
- ✅ 数据自动修复机制
- ✅ 响应式设计
- ✅ 健康检查
- ✅ 静态资源缓存优化
### 📁 项目结构
```
绩效计分系统7.24/
├── Dockerfile # Docker 构建文件
├── nginx.conf # Nginx 配置 (SPA 回退)
├── docker-compose.yml # Docker Compose 配置
├── docker-deploy.bat # 一键部署脚本
├── docker-manage.bat # 容器管理脚本
├── dist/ # 构建产物
└── src/ # 源代码
```
### 🚀 生产环境部署建议
#### 1. 使用 Docker Compose
```bash
docker-compose up -d
```
#### 2. 自定义端口
修改 docker-compose.yml 中的端口映射:
```yaml
ports:
- "8080:80" # 改为 8080 端口
```
#### 3. 数据持久化
如需数据持久化,可挂载数据目录:
```yaml
volumes:
- ./data:/app/data
```
### 🔍 故障排除
#### 端口被占用
```bash
# 查看端口占用
netstat -ano | findstr :3000
# 使用其他端口
docker run -d -p 8080:80 --name scoring-system-8.8 scoring-system:8.8
```
#### 容器无法启动
```bash
# 查看详细日志
docker logs scoring-system-8.8
# 重新构建镜像
docker build -t scoring-system:8.8 .
```
### 📞 支持信息
- **版本**: 8.8.0
- **构建时间**: 2025-08-08
- **技术栈**: Vue 3 + Element Plus + Nginx
- **容器化**: Docker + Alpine Linux
---
## 🎯 下一步
系统已成功部署并运行,你可以:
1. 访问 http://localhost:3000/ 开始使用
2. 使用管理员账号登录查看所有功能
3. 测试图片上传和删除功能
4. 根据需要调整端口或配置
# 绩效计分系统版本历史
## 版本 8.8 (2025-08-08)1
### 重要变更
- 🖼️ 修复图片上传归属错乱与删除后不更新问题
- 🔒 强化权限校验:上传/删除仅限所属机构
- ⚙️ 启动脚本升级:一键释放端口并启用 SPA 回退,无刷新 404
- 🧰 数据自检与修复:加载时自动修复机构内部ID冲突
- 🐳 新增 Docker 部署支持(生产构建 + Nginx SPA 回退)
### 技术栈
- Vue 3.4.29
- Element Plus 2.7.6
- Vite 5.4.19
- Node.js 环境
### 部署说明
- 支持本地 Node 静态服务器(serve-dist.js)
- 新增 Docker 容器化部署(见根目录 Dockerfile 与 docker-compose.yml)
---
## 版本 8.4 (2025-08-04)
### 新增功能
......@@ -32,8 +53,7 @@
---
## 下一版本计划 (8.5)
- 🐳 Docker生产环境部署
## 下一版本计划 (8.9)
- 🔄 多用户多浏览器数据同步
- 📡 WebSocket实时通信
- 🔐 增强安全性和权限管理
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 8080;
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 处理CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
let filePath = req.url === '/' ? '/index.html' : req.url;
// 移除查询参数
filePath = filePath.split('?')[0];
// 安全检查,防止目录遍历
if (filePath.includes('..')) {
res.writeHead(400);
res.end('Bad Request');
return;
}
// 确定文件的完整路径
let fullPath;
if (filePath.startsWith('/public/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/assets/')) {
fullPath = path.join(__dirname, 'dist', filePath);
} else if (filePath === '/index.html') {
fullPath = path.join(__dirname, 'dist', 'index.html');
} else {
// 尝试在dist目录中查找
fullPath = path.join(__dirname, 'dist', filePath);
if (!fs.existsSync(fullPath)) {
// 如果在dist中找不到,尝试public目录
fullPath = path.join(__dirname, 'public', filePath);
if (!fs.existsSync(fullPath)) {
// 最后尝试根目录
fullPath = path.join(__dirname, filePath.substring(1));
}
}
}
// 检查文件是否存在
fs.access(fullPath, fs.constants.F_OK, (err) => {
if (err) {
console.log(`文件不存在: ${fullPath}`);
res.writeHead(404);
res.end('File Not Found');
return;
}
// 获取文件扩展名
const ext = path.extname(fullPath).toLowerCase();
const mimeType = mimeTypes[ext] || 'application/octet-stream';
// 读取并发送文件
fs.readFile(fullPath, (err, data) => {
if (err) {
console.error(`读取文件错误: ${err}`);
res.writeHead(500);
res.end('Internal Server Error');
return;
}
res.writeHead(200, { 'Content-Type': mimeType });
res.end(data);
});
});
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`🚀 绩效计分系统 v8.6 服务器已启动`);
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主系统: http://localhost:${PORT}/index.html`);
console.log(`🔍 诊断页面: http://localhost:${PORT}/public/diagnose.html`);
console.log('');
console.log('🎉 v8.6版本已启动,现在可以体验新功能!');
console.log('');
console.log('按 Ctrl+C 停止服务器');
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 ${PORT} 已被占用,请关闭其他服务或使用其他端口`);
} else {
console.error(`❌ 服务器错误: ${err}`);
}
});
#!/usr/bin/env python3
import http.server
import socketserver
import os
import sys
from pathlib import Path
PORT = 5174
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(Path(__file__).parent), **kwargs)
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
super().end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.end_headers()
def do_GET(self):
# 处理SPA路由,所有非文件请求都返回index.html
if not os.path.exists(self.translate_path(self.path)) and not self.path.startswith('/src/') and not self.path.startswith('/public/') and not self.path.startswith('/node_modules/'):
self.path = '/index.html'
super().do_GET()
def main():
try:
with socketserver.TCPServer(("127.0.0.1", PORT), MyHTTPRequestHandler) as httpd:
print(f"🚀 绩效计分系统 v8.6 开发服务器已启动")
print(f"📍 服务器地址: http://localhost:{PORT}")
print(f"🏠 主系统: http://localhost:{PORT}/index.html")
print("")
print("🎉 v8.6版本已启动,现在可以体验新功能!")
print("")
print("默认登录账号:")
print("- 管理员: admin / admin123")
print("- 陈锐屏: 13800138001 / 123456")
print("- 张田田: 13800138002 / 123456")
print("- 余芳飞: 13800138003 / 123456")
print("")
print("按 Ctrl+C 停止服务器")
print("=" * 50)
httpd.serve_forever()
except KeyboardInterrupt:
print("\n服务器已停止")
except OSError as e:
if e.errno == 10048: # Address already in use
print(f"❌ 端口 {PORT} 已被占用,请关闭其他服务或使用其他端口")
else:
print(f"❌ 服务器错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
// 诊断数据一致性问题
console.log('=== 诊断数据一致性问题 ===');
// 模拟浏览器环境中的localStorage
const localStorage = {
getItem: (key) => {
// 这里需要在浏览器控制台中运行
return null;
}
};
// 检查函数
function diagnoseDataConsistency() {
console.log('请在浏览器控制台中运行以下代码:');
console.log(`
// 1. 检查localStorage中的数据
const usersData = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutionsData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
console.log('=== 数据一致性诊断 ===');
console.log('\\n1️⃣ 用户数据检查:');
console.log('用户总数:', usersData.length);
usersData.forEach(user => {
if (user.role === 'user') {
console.log('用户:', user.name, '(ID:', user.id + ')');
// 查找该用户负责的机构
const userInstitutions = institutionsData.filter(inst => inst.ownerId === user.id);
console.log(' 负责机构数量:', userInstitutions.length);
userInstitutions.forEach(inst => {
console.log(' -', inst.name, '(内部ID:', inst.id, ', 机构编号:', inst.institutionId + ')');
});
}
});
console.log('\\n2️⃣ 机构数据检查:');
console.log('机构总数:', institutionsData.length);
// 检查是否有机构归属错误
const problemInstitutions = [];
institutionsData.forEach(inst => {
if (inst.ownerId) {
const owner = usersData.find(u => u.id === inst.ownerId);
if (!owner) {
problemInstitutions.push({
name: inst.name,
ownerId: inst.ownerId,
issue: '负责人不存在'
});
}
}
});
if (problemInstitutions.length > 0) {
console.log('\\n⚠️ 发现问题机构:');
problemInstitutions.forEach(prob => {
console.log(' -', prob.name, ':', prob.issue, '(负责人ID:', prob.ownerId + ')');
});
}
console.log('\\n3️⃣ 检查特定用户张田田的数据:');
const zhangTianTian = usersData.find(u => u.name === '张田田');
if (zhangTianTian) {
console.log('张田田用户ID:', zhangTianTian.id);
const zhangInstitutions = institutionsData.filter(inst => inst.ownerId === zhangTianTian.id);
console.log('张田田负责的机构:');
zhangInstitutions.forEach(inst => {
console.log(' -', inst.name, '(内部ID:', inst.id, ', 机构编号:', inst.institutionId + ')');
});
// 检查武夷山思美达口腔门诊部
const wuyishanInst = institutionsData.find(inst => inst.name.includes('武夷山思美达'));
if (wuyishanInst) {
console.log('\\n武夷山思美达口腔门诊部信息:');
console.log(' 名称:', wuyishanInst.name);
console.log(' 内部ID:', wuyishanInst.id);
console.log(' 机构编号:', wuyishanInst.institutionId);
console.log(' 负责人ID:', wuyishanInst.ownerId);
const actualOwner = usersData.find(u => u.id === wuyishanInst.ownerId);
console.log(' 实际负责人:', actualOwner ? actualOwner.name : '未找到');
if (wuyishanInst.ownerId !== zhangTianTian.id) {
console.log(' ❌ 权限问题: 张田田无权操作此机构');
console.log(' ❌ 这就是报错的原因!');
} else {
console.log(' ✅ 权限正常: 张田田可以操作此机构');
}
} else {
console.log('\\n❌ 未找到武夷山思美达口腔门诊部');
}
} else {
console.log('❌ 未找到用户张田田');
}
console.log('\\n4️⃣ 检查崇川区海虹口腔门诊部:');
const chongchuanInst = institutionsData.find(inst => inst.name.includes('崇川区海虹'));
if (chongchuanInst) {
console.log('崇川区海虹口腔门诊部信息:');
console.log(' 名称:', chongchuanInst.name);
console.log(' 内部ID:', chongchuanInst.id);
console.log(' 机构编号:', chongchuanInst.institutionId);
console.log(' 负责人ID:', chongchuanInst.ownerId);
const actualOwner = usersData.find(u => u.id === chongchuanInst.ownerId);
console.log(' 实际负责人:', actualOwner ? actualOwner.name : '未找到');
} else {
console.log('❌ 未找到崇川区海虹口腔门诊部');
}
console.log('\\n5️⃣ 检查机构ID重复问题:');
const institutionIdMap = new Map();
const duplicateIds = [];
institutionsData.forEach(inst => {
if (inst.institutionId) {
if (institutionIdMap.has(inst.institutionId)) {
duplicateIds.push({
id: inst.institutionId,
institutions: [institutionIdMap.get(inst.institutionId), inst]
});
} else {
institutionIdMap.set(inst.institutionId, inst);
}
}
});
if (duplicateIds.length > 0) {
console.log('⚠️ 发现重复机构ID:');
duplicateIds.forEach(dup => {
console.log(' 重复ID:', dup.id);
dup.institutions.forEach((inst, index) => {
console.log(' 机构' + (index + 1) + ':', inst.name, '(负责人:', inst.ownerId + ')');
});
});
} else {
console.log('✅ 未发现机构ID重复问题');
}
console.log('\\n=== 诊断完成 ===');
`);
}
// 如果在Node.js环境中运行,显示提示
if (typeof window === 'undefined') {
diagnoseDataConsistency();
} else {
// 在浏览器中直接执行诊断
// 这部分代码会在浏览器控制台中运行
}
// 诊断图片显示错乱问题
console.log('=== 诊断图片显示错乱问题 ===');
// 1. 检查localStorage中的数据
const usersData = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutionsData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
console.log('\n1️⃣ 基础数据检查:');
console.log('用户总数:', usersData.length);
console.log('机构总数:', institutionsData.length);
// 2. 检查张田田的机构归属
const zhangTianTian = usersData.find(user => user.name === '张田田');
if (zhangTianTian) {
console.log('\n2️⃣ 张田田用户信息:');
console.log('用户ID:', zhangTianTian.id);
const zhangInstitutions = institutionsData.filter(inst => inst.ownerId === zhangTianTian.id);
console.log('负责机构数量:', zhangInstitutions.length);
zhangInstitutions.forEach(inst => {
console.log(` - ${inst.name} (内部ID: ${inst.id}, 机构编号: ${inst.institutionId})`);
console.log(` 图片数量: ${inst.images ? inst.images.length : 0}`);
if (inst.images && inst.images.length > 0) {
inst.images.forEach((img, index) => {
console.log(` 图片${index + 1}: ${img.name} (ID: ${img.id})`);
});
}
});
} else {
console.log('\n❌ 未找到用户张田田');
}
// 3. 检查武夷山思美达口腔门诊部
console.log('\n3️⃣ 武夷山思美达口腔门诊部检查:');
const wuyishanInst = institutionsData.find(inst =>
inst.name.includes('武夷山思美达') || inst.name.includes('思美达')
);
if (wuyishanInst) {
console.log('机构信息:');
console.log(' 名称:', wuyishanInst.name);
console.log(' 内部ID:', wuyishanInst.id);
console.log(' 机构编号:', wuyishanInst.institutionId);
console.log(' 负责人ID:', wuyishanInst.ownerId);
console.log(' 图片数量:', wuyishanInst.images ? wuyishanInst.images.length : 0);
if (wuyishanInst.images && wuyishanInst.images.length > 0) {
console.log(' 图片列表:');
wuyishanInst.images.forEach((img, index) => {
console.log(` ${index + 1}. ${img.name} (ID: ${img.id})`);
console.log(` 上传时间: ${img.uploadTime}`);
});
}
const actualOwner = usersData.find(u => u.id === wuyishanInst.ownerId);
console.log(' 实际负责人:', actualOwner ? actualOwner.name : '未找到');
} else {
console.log('❌ 未找到武夷山思美达口腔门诊部');
}
// 4. 检查温州奥齿泰口腔门诊部
console.log('\n4️⃣ 温州奥齿泰口腔门诊部检查:');
const wenzhouInst = institutionsData.find(inst =>
inst.name.includes('温州奥齿泰') || inst.name.includes('奥齿泰')
);
if (wenzhouInst) {
console.log('机构信息:');
console.log(' 名称:', wenzhouInst.name);
console.log(' 内部ID:', wenzhouInst.id);
console.log(' 机构编号:', wenzhouInst.institutionId);
console.log(' 负责人ID:', wenzhouInst.ownerId);
console.log(' 图片数量:', wenzhouInst.images ? wenzhouInst.images.length : 0);
if (wenzhouInst.images && wenzhouInst.images.length > 0) {
console.log(' 图片列表:');
wenzhouInst.images.forEach((img, index) => {
console.log(` ${index + 1}. ${img.name} (ID: ${img.id})`);
console.log(` 上传时间: ${img.uploadTime}`);
});
}
const actualOwner = usersData.find(u => u.id === wenzhouInst.ownerId);
console.log(' 实际负责人:', actualOwner ? actualOwner.name : '未找到');
} else {
console.log('❌ 未找到温州奥齿泰口腔门诊部');
}
// 5. 检查是否有机构ID冲突
console.log('\n5️⃣ 机构ID冲突检查:');
const institutionIdMap = new Map();
const duplicateIds = [];
institutionsData.forEach(inst => {
if (institutionIdMap.has(inst.id)) {
duplicateIds.push({
id: inst.id,
institutions: [institutionIdMap.get(inst.id), inst]
});
} else {
institutionIdMap.set(inst.id, inst);
}
});
if (duplicateIds.length > 0) {
console.log('⚠️ 发现重复的机构内部ID:');
duplicateIds.forEach(dup => {
console.log(` ID: ${dup.id}`);
dup.institutions.forEach((inst, index) => {
console.log(` ${index + 1}. ${inst.name} (负责人: ${inst.ownerId})`);
});
});
} else {
console.log('✅ 未发现机构ID冲突');
}
// 6. 检查图片数据完整性
console.log('\n6️⃣ 图片数据完整性检查:');
let totalImages = 0;
let invalidImages = 0;
institutionsData.forEach(inst => {
if (inst.images && Array.isArray(inst.images)) {
totalImages += inst.images.length;
inst.images.forEach(img => {
if (!img.id || !img.url || !img.name) {
invalidImages++;
console.log(`⚠️ 无效图片在机构 ${inst.name}:`, img);
}
});
}
});
console.log(`总图片数量: ${totalImages}`);
console.log(`无效图片数量: ${invalidImages}`);
// 7. 检查响应式数据状态(如果在Vue环境中)
if (typeof window !== 'undefined' && window.dataStore) {
console.log('\n7️⃣ Vue响应式数据检查:');
const storeInstitutions = window.dataStore.getInstitutions();
console.log('Store中机构数量:', storeInstitutions.length);
if (zhangTianTian) {
const storeUserInstitutions = window.dataStore.getInstitutionsByUserId(zhangTianTian.id);
console.log('Store中张田田的机构数量:', storeUserInstitutions.length);
}
}
console.log('\n=== 诊断完成 ===');
/**
* 诊断图片上传问题的脚本
* 检查机构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) - 检查特定上传权限')
}
version: '3.8'
version: "3.8"
services:
# 主应用服务
app:
scoring-app:
build:
context: .
dockerfile: Dockerfile
container_name: performance-system
restart: unless-stopped
container_name: scoring-app-8.8
ports:
- "8080:80"
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- REDIS_URL=redis://redis:6379
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=performance_db
- DB_USER=performance_user
- DB_PASSWORD=performance_pass
depends_on:
- redis
- postgres
volumes:
- app_data:/app/data
- app_logs:/var/log/nginx
networks:
- performance_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Redis缓存服务
redis:
image: redis:7-alpine
container_name: performance-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- performance_network
command: redis-server --appendonly yes --requirepass redis_password
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# PostgreSQL数据库服务
postgres:
image: postgres:15-alpine
container_name: performance-postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
- POSTGRES_DB=performance_db
- POSTGRES_USER=performance_user
- POSTGRES_PASSWORD=performance_pass
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- performance_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U performance_user -d performance_db"]
interval: 30s
timeout: 10s
retries: 3
# Nginx负载均衡器(可选,用于多实例部署)
nginx:
image: nginx:alpine
container_name: performance-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.prod.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
- nginx_logs:/var/log/nginx
depends_on:
- app
networks:
- performance_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
# 数据卷
volumes:
app_data:
driver: local
app_logs:
driver: local
redis_data:
driver: local
postgres_data:
driver: local
nginx_logs:
driver: local
# 网络
networks:
performance_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
- NODE_ENV=production
# 如果后续需要挂载静态资源或配置:
# volumes:
# - ./some-dir:/usr/share/nginx/html/some-dir:ro
@echo off
setlocal ENABLEDELAYEDEXPANSION
cd /d "%~dp0"
echo ========================================
echo 绩效计分系统 8.8 版本 - Docker 部署
echo ========================================
set CONTAINER_NAME=scoring-system-8.8
set IMAGE_NAME=scoring-system:8.8
set PORT=3000
echo [1/4] 检查 Docker 环境...
docker --version >nul 2>&1
if errorlevel 1 (
echo ❌ Docker 未安装或未启动
pause
exit /b 1
)
echo [2/4] 停止并移除旧容器(如果存在)...
docker stop %CONTAINER_NAME% >nul 2>&1
docker rm %CONTAINER_NAME% >nul 2>&1
echo [3/4] 构建最新镜像...
docker build -t %IMAGE_NAME% .
if errorlevel 1 (
echo ❌ 镜像构建失败
pause
exit /b 1
)
echo [4/4] 启动容器...
docker run -d -p %PORT%:80 --name %CONTAINER_NAME% %IMAGE_NAME%
if errorlevel 1 (
echo ❌ 容器启动失败
pause
exit /b 1
)
echo.
echo ✅ 部署成功!
echo.
echo 🌐 访问地址: http://localhost:%PORT%/
echo 📊 容器状态: docker ps
echo 📋 查看日志: docker logs %CONTAINER_NAME%
echo 🛑 停止服务: docker stop %CONTAINER_NAME%
echo.
echo 默认登录账号:
echo - 管理员: admin / admin123
echo - 张田田: 13800138002 / 123456
echo - 陈锐屏: 13800138001 / 123456
echo - 余芳飞: 13800138003 / 123456
echo.
pause
@echo off
setlocal ENABLEDELAYEDEXPANSION
cd /d "%~dp0"
set CONTAINER_NAME=scoring-system-8.8
set IMAGE_NAME=scoring-system:8.8
:menu
echo.
echo ========================================
echo 绩效计分系统 8.8 版本 - Docker 管理
echo ========================================
echo.
echo 1. 查看容器状态
echo 2. 查看容器日志
echo 3. 重启容器
echo 4. 停止容器
echo 5. 启动容器
echo 6. 删除容器
echo 7. 重新构建并部署
echo 8. 打开浏览器访问
echo 0. 退出
echo.
set /p choice=请选择操作 (0-8):
if "%choice%"=="1" goto status
if "%choice%"=="2" goto logs
if "%choice%"=="3" goto restart
if "%choice%"=="4" goto stop
if "%choice%"=="5" goto start
if "%choice%"=="6" goto remove
if "%choice%"=="7" goto rebuild
if "%choice%"=="8" goto open
if "%choice%"=="0" goto exit
echo 无效选择,请重试
goto menu
:status
echo.
echo 容器状态:
docker ps -a --filter name=%CONTAINER_NAME%
goto menu
:logs
echo.
echo 容器日志 (最近50行):
docker logs --tail 50 %CONTAINER_NAME%
goto menu
:restart
echo.
echo 重启容器...
docker restart %CONTAINER_NAME%
echo 重启完成
goto menu
:stop
echo.
echo 停止容器...
docker stop %CONTAINER_NAME%
echo 停止完成
goto menu
:start
echo.
echo 启动容器...
docker start %CONTAINER_NAME%
echo 启动完成
goto menu
:remove
echo.
echo 删除容器...
docker stop %CONTAINER_NAME% >nul 2>&1
docker rm %CONTAINER_NAME%
echo 删除完成
goto menu
:rebuild
echo.
echo 重新构建并部署...
docker stop %CONTAINER_NAME% >nul 2>&1
docker rm %CONTAINER_NAME% >nul 2>&1
docker build -t %IMAGE_NAME% .
docker run -d -p 3000:80 --name %CONTAINER_NAME% %IMAGE_NAME%
echo 重新部署完成
goto menu
:open
echo.
echo 打开浏览器...
start http://localhost:3000/
goto menu
:exit
echo 退出管理工具
exit /b 0
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>绩效计分系统 v8.6 优化版本</title>
<!-- Element Plus CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.7.6/dist/index.css">
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3.4.29/dist/vue.global.js"></script>
<!-- Element Plus -->
<script src="https://unpkg.com/element-plus@2.7.6/dist/index.full.js"></script>
<!-- Element Plus Icons -->
<script src="https://unpkg.com/@element-plus/icons-vue@2.3.1/dist/index.iife.min.js"></script>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.loading-title {
font-size: 32px;
font-weight: bold;
margin-bottom: 16px;
}
.loading-subtitle {
font-size: 18px;
margin-bottom: 32px;
opacity: 0.9;
}
.loading-features {
text-align: left;
margin-bottom: 32px;
}
.loading-features h3 {
margin-bottom: 16px;
font-size: 20px;
}
.loading-features ul {
list-style: none;
padding: 0;
}
.loading-features li {
margin-bottom: 8px;
padding-left: 24px;
position: relative;
}
.loading-features li:before {
content: "✨";
position: absolute;
left: 0;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: #f56c6c;
color: white;
text-align: center;
padding: 20px;
}
.error-title {
font-size: 24px;
margin-bottom: 16px;
}
.error-message {
font-size: 16px;
margin-bottom: 24px;
max-width: 600px;
}
.error-solutions {
text-align: left;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 8px;
max-width: 600px;
}
.error-solutions h3 {
margin-top: 0;
}
.error-solutions ol {
margin: 0;
}
.error-solutions li {
margin-bottom: 8px;
}
</style>
</head>
<body>
<!-- 加载界面 -->
<div id="loading" class="loading-container">
<div class="loading-title">绩效计分系统</div>
<div class="loading-subtitle">v8.6 优化版本正在启动...</div>
<div class="loading-features">
<h3>🚀 v8.6 优化功能</h3>
<ul>
<li>智能图片压缩 (减少50-70%存储)</li>
<li>实时存储监控和自动清理</li>
<li>自动备份恢复系统</li>
<li>增强的图片上传体验</li>
<li>完善的数据分离机制</li>
</ul>
</div>
<div class="loading-spinner"></div>
</div>
<!-- 错误界面 -->
<div id="error" class="error-container">
<div class="error-title">⚠️ 系统启动失败</div>
<div class="error-message">
由于浏览器安全限制,系统无法在文件协议下正常运行。<br>
请使用HTTP服务器访问系统以获得完整功能。
</div>
<div class="error-solutions">
<h3>🔧 解决方案</h3>
<ol>
<li><strong>推荐方式</strong>:运行 <code>启动v86优化版本.bat</code> 脚本</li>
<li>或者在命令行运行:<code>python -m http.server 5174</code></li>
<li>然后访问:<code>http://localhost:5174</code></li>
<li>如果没有Python,可以安装Node.js后运行:<code>npx serve .</code></li>
</ol>
</div>
</div>
<!-- 应用容器 -->
<div id="app" style="display: none;"></div>
<script>
// 检测是否为文件协议
if (window.location.protocol === 'file:') {
// 延迟显示错误信息
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'flex';
}, 3000);
} else {
// HTTP协议下正常加载
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
document.getElementById('app').style.display = 'block';
// 这里应该加载实际的Vue应用
// 由于无法在standalone版本中加载模块,显示提示信息
document.getElementById('app').innerHTML = `
<div style="padding: 40px; text-align: center; font-family: Arial, sans-serif;">
<h1 style="color: #409eff; margin-bottom: 20px;">🎉 系统启动成功!</h1>
<p style="font-size: 18px; margin-bottom: 30px;">
绩效计分系统 v8.6 优化版本已准备就绪
</p>
<div style="background: #f0f9ff; padding: 20px; border-radius: 8px; margin-bottom: 30px; border-left: 4px solid #409eff;">
<h3 style="margin-top: 0; color: #409eff;">⚡ v8.6 优化功能已启用</h3>
<ul style="text-align: left; max-width: 400px; margin: 0 auto;">
<li>✨ 智能图片压缩 (减少50-70%存储)</li>
<li>📊 实时存储监控</li>
<li>💾 自动备份恢复</li>
<li>🎨 增强用户体验</li>
<li>🛡️ 数据分离优化</li>
</ul>
</div>
<div style="background: #fff7e6; padding: 20px; border-radius: 8px; border-left: 4px solid #e6a23c;">
<h3 style="margin-top: 0; color: #e6a23c;">🔧 完整功能访问</h3>
<p>要体验完整的v8.6优化功能,请:</p>
<ol style="text-align: left; max-width: 500px; margin: 0 auto;">
<li>确保使用HTTP服务器访问(不是file://协议)</li>
<li>运行 <code>启动v86优化版本.bat</code> 脚本</li>
<li>或访问:<a href="http://localhost:5174" target="_blank">http://localhost:5174</a></li>
</ol>
</div>
<div style="margin-top: 30px;">
<button onclick="window.location.reload()"
style="background: #409eff; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer;">
🔄 重新加载
</button>
</div>
</div>
`;
}, 2000);
}
</script>
</body>
</html>
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# 基本设置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# 上游服务器配置
upstream backend {
server localhost:3000;
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# 静态文件缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API代理
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# WebSocket代理
location /socket.io/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# SPA路由支持
location / {
try_files $uri $uri/ /index.html;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
# Nginx config for SPA (Vue Router history mode)
server {
listen 80;
server_name _;
# 静态资源
root /usr/share/nginx/html;
index index.html;
# 处理前端路由:不存在的路径回退 index.html
location / {
try_files $uri $uri/ /index.html;
}
# 缓存与压缩(可按需调整)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
}
/**
* 数据清理脚本
* 在浏览器控制台中运行此脚本来清理数据
*/
// 清理所有非管理员数据
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 http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = 8080;
const DIST_DIR = path.join(__dirname, 'dist');
// MIME types
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
let filePath = path.join(DIST_DIR, req.url === '/' ? 'index.html' : req.url);
// Remove query parameters
filePath = filePath.split('?')[0];
// Security check - prevent directory traversal
if (!filePath.startsWith(DIST_DIR)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
const extname = String(path.extname(filePath)).toLowerCase();
const mimeType = mimeTypes[extname] || 'application/octet-stream';
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code === 'ENOENT') {
// File not found - serve index.html for SPA routing
fs.readFile(path.join(DIST_DIR, 'index.html'), (error, content) => {
if (error) {
res.writeHead(500);
res.end('Server Error: ' + error.code);
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content, 'utf-8');
}
});
} else {
res.writeHead(500);
res.end('Server Error: ' + error.code);
}
} else {
res.writeHead(200, { 'Content-Type': mimeType });
res.end(content, 'utf-8');
}
});
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
console.log(`Serving files from: ${DIST_DIR}`);
});
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 3000;
// MIME类型映射
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'font/otf'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 处理CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
let filePath = req.url === '/' ? '/index.html' : req.url;
// 移除查询参数
filePath = filePath.split('?')[0];
// 安全检查,防止目录遍历
if (filePath.includes('..')) {
res.writeHead(400);
res.end('Bad Request');
return;
}
// 确定文件的完整路径
let fullPath;
if (filePath.startsWith('/src/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/public/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/node_modules/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath === '/index.html') {
fullPath = path.join(__dirname, 'index.html');
} else {
// 尝试在根目录中查找
fullPath = path.join(__dirname, filePath.substring(1));
if (!fs.existsSync(fullPath)) {
// 如果找不到,对于SPA路由返回index.html
if (!path.extname(filePath)) {
fullPath = path.join(__dirname, 'index.html');
}
}
}
// 检查文件是否存在
fs.access(fullPath, fs.constants.F_OK, (err) => {
if (err) {
console.log(`文件不存在: ${fullPath}`);
// 对于SPA路由,返回index.html
if (!path.extname(filePath)) {
fullPath = path.join(__dirname, 'index.html');
serveFile(fullPath, res);
} else {
res.writeHead(404);
res.end('File Not Found');
}
return;
}
serveFile(fullPath, res);
});
});
function serveFile(fullPath, res) {
// 获取文件扩展名
const ext = path.extname(fullPath).toLowerCase();
const mimeType = mimeTypes[ext] || 'application/octet-stream';
// 读取并发送文件
fs.readFile(fullPath, (err, data) => {
if (err) {
console.error(`读取文件错误: ${err}`);
res.writeHead(500);
res.end('Internal Server Error');
return;
}
res.writeHead(200, { 'Content-Type': mimeType });
res.end(data);
});
}
server.listen(PORT, 'localhost', () => {
console.log(`🚀 绩效计分系统 v8.6 开发服务器已启动`);
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主系统: http://localhost:${PORT}/index.html`);
console.log('');
console.log('🎉 v8.6版本已启动,现在可以体验新功能!');
console.log('');
console.log('默认登录账号:');
console.log('- 管理员: admin / admin123');
console.log('- 陈锐屏: 13800138001 / 123456');
console.log('- 张田田: 13800138002 / 123456');
console.log('- 余芳飞: 13800138003 / 123456');
console.log('');
console.log('按 Ctrl+C 停止服务器');
console.log('=' + '='.repeat(49));
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 ${PORT} 已被占用,请关闭其他服务或使用其他端口`);
} else {
console.error(`❌ 服务器错误: ${err}`);
}
});
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 5174;
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 处理CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
let filePath = req.url === '/' ? '/index.html' : req.url;
// 移除查询参数
filePath = filePath.split('?')[0];
// 安全检查,防止目录遍历
if (filePath.includes('..')) {
res.writeHead(400);
res.end('Bad Request');
return;
}
// 确定文件的完整路径
let fullPath;
if (filePath.startsWith('/public/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/src/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/node_modules/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath === '/index.html') {
fullPath = path.join(__dirname, 'index.html');
} else {
// 尝试在public目录中查找
fullPath = path.join(__dirname, 'public', filePath);
if (!fs.existsSync(fullPath)) {
// 如果在public中找不到,尝试根目录
fullPath = path.join(__dirname, filePath.substring(1));
}
}
// 检查文件是否存在
fs.access(fullPath, fs.constants.F_OK, (err) => {
if (err) {
console.log(`文件不存在: ${fullPath}`);
res.writeHead(404);
res.end('File Not Found');
return;
}
// 获取文件扩展名
const ext = path.extname(fullPath).toLowerCase();
const mimeType = mimeTypes[ext] || 'application/octet-stream';
// 读取并发送文件
fs.readFile(fullPath, (err, data) => {
if (err) {
console.error(`读取文件错误: ${err}`);
res.writeHead(500);
res.end('Internal Server Error');
return;
}
res.writeHead(200, { 'Content-Type': mimeType });
res.end(data);
});
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 绩效计分系统 v8.7 服务器已启动`);
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主系统: http://localhost:${PORT}/index.html`);
console.log(`🧪 测试页面: http://localhost:${PORT}/public/test-v87.html`);
console.log(`🔧 升级工具: http://localhost:${PORT}/public/upgrade-v87.html`);
console.log(`🔍 诊断页面: http://localhost:${PORT}/public/diagnose.html`);
console.log('');
console.log('🎉 v8.7架构升级已完成,现在可以体验新功能!');
console.log('');
console.log('按 Ctrl+C 停止服务器');
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 ${PORT} 已被占用,请关闭其他服务或使用其他端口`);
} else {
console.error(`❌ 服务器错误: ${err}`);
}
});
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = 8080;
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 处理CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
let filePath = req.url === '/' ? '/index.html' : req.url;
// 移除查询参数
filePath = filePath.split('?')[0];
// 安全检查,防止目录遍历
if (filePath.includes('..')) {
res.writeHead(400);
res.end('Bad Request');
return;
}
// 确定文件的完整路径
let fullPath;
if (filePath.startsWith('/public/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/src/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/node_modules/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath === '/index.html') {
fullPath = path.join(__dirname, 'index.html');
} else {
// 尝试在public目录中查找
fullPath = path.join(__dirname, 'public', filePath);
if (!fs.existsSync(fullPath)) {
// 如果在public中找不到,尝试根目录
fullPath = path.join(__dirname, filePath.substring(1));
}
}
// 检查文件是否存在
fs.access(fullPath, fs.constants.F_OK, (err) => {
if (err) {
console.log(`文件不存在: ${fullPath}`);
res.writeHead(404);
res.end('File Not Found');
return;
}
// 获取文件扩展名
const ext = path.extname(fullPath).toLowerCase();
const mimeType = mimeTypes[ext] || 'application/octet-stream';
// 读取并发送文件
fs.readFile(fullPath, (err, data) => {
if (err) {
console.error(`读取文件错误: ${err}`);
res.writeHead(500);
res.end('Internal Server Error');
return;
}
res.writeHead(200, { 'Content-Type': mimeType });
res.end(data);
});
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 绩效计分系统 v8.7 服务器已启动`);
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主系统: http://localhost:${PORT}/index.html`);
console.log(`🧪 测试页面: http://localhost:${PORT}/public/test-v87.html`);
console.log(`🔧 升级工具: http://localhost:${PORT}/public/upgrade-v87.html`);
console.log(`🔍 诊断页面: http://localhost:${PORT}/public/diagnose.html`);
console.log('');
console.log('🎉 v8.7架构升级已完成,现在可以体验新功能!');
console.log('');
console.log('按 Ctrl+C 停止服务器');
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 ${PORT} 已被占用,请关闭其他服务或使用其他端口`);
} else {
console.error(`❌ 服务器错误: ${err}`);
}
});
/**
* 数据清理工具
* 用于清理系统中的用户和机构数据,只保留管理员
*/
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
@echo off
chcp 65001 >nul
echo ========================================
echo 绩效计分系统 v8.6 - 开发服务器
echo ========================================
echo.
:: 检查 Node.js 是否安装
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ 错误: Node.js 未安装
pause
exit /b 1
)
echo ✅ Node.js 环境检查通过
node --version
:: 检查 npm 是否可用
npm --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ 错误: npm 不可用
pause
exit /b 1
)
echo ✅ npm 环境检查通过
npm --version
echo.
:: 检查是否已安装依赖
if not exist "node_modules" (
echo 📦 正在安装项目依赖...
npm install
if %errorlevel% neq 0 (
echo ❌ 依赖安装失败
pause
exit /b 1
)
)
echo 🚀 正在启动开发服务器...
echo.
echo 启动成功后,请在浏览器中访问显示的地址
echo.
echo 默认登录账号:
echo - 管理员: admin / admin123
echo - 陈锐屏: 13800138001 / 123456
echo - 张田田: 13800138002 / 123456
echo - 余芳飞: 13800138003 / 123456
echo.
echo 按 Ctrl+C 可停止服务器
echo ========================================
echo.
:: 直接调用vite
.\node_modules\.bin\vite.cmd --host 127.0.0.1 --port 5174
pause
@echo off
chcp 65001 >nul
echo ========================================
echo 绩效计分系统 v8.6 - 开发服务器
echo ========================================
echo.
REM 检查Node.js是否安装
node --version >nul 2>&1
if errorlevel 1 (
echo ❌ 错误: 未找到Node.js,请先安装Node.js
echo 请访问 https://nodejs.org/ 下载安装
pause
exit /b 1
)
echo ✅ Node.js 环境检查通过
node --version
REM 检查npm是否可用
npm --version >nul 2>&1
if errorlevel 1 (
echo ❌ 错误: 未找到npm,请检查Node.js安装
pause
exit /b 1
)
echo ✅ npm 环境检查通过
npm --version
echo.
REM 检查node_modules是否存在
if not exist "node_modules" (
echo 📦 正在安装项目依赖...
echo 这可能需要几分钟时间,请耐心等待...
npm install
if errorlevel 1 (
echo ❌ 依赖安装失败,请检查网络连接
pause
exit /b 1
)
echo ✅ 依赖安装完成
)
echo.
echo 🚀 启动绩效计分系统 v8.6 开发服务器...
echo.
echo 📍 服务器地址: http://localhost:8080
echo 🏠 主系统: http://localhost:8080
echo 🔍 诊断页面: http://localhost:8080/public/diagnose.html
echo.
echo 默认登录账号:
echo - 管理员: admin / admin123
echo - 陈锐屏: 13800138001 / 123456
echo - 张田田: 13800138002 / 123456
echo - 余芳飞: 13800138003 / 123456
echo.
echo 按 Ctrl+C 停止服务器
echo ========================================
echo.
REM 尝试启动vite开发服务器,使用不同端口避免权限问题
echo 正在启动vite开发服务器...
npx vite --port 8080 --host 127.0.0.1
REM 如果vite失败,使用简单HTTP服务器
if errorlevel 1 (
echo.
echo ⚠️ vite启动失败,使用备用HTTP服务器...
node basic-server.js
)
pause
@echo off
setlocal ENABLEDELAYEDEXPANSION
chcp 65001 >nul
cd /d "%~dp0"
echo ========================================
echo 绩效计分系统 8.8 - Docker 启动
echo ========================================
REM 1) 检查 Docker 可用
where docker >nul 2>&1
if errorlevel 1 (
echo ❌ 未检测到 Docker,请先安装 Docker Desktop 并启动后重试
pause
exit /b 1
)
REM 2) 检查 Docker Engine 是否运行
for /f "tokens=2 delims=:" %%a in ('docker info 2^>^&1 ^| findstr /C:"Server Version"') do set DOCKER_RUNNING=1
if not defined DOCKER_RUNNING (
echo ❌ 检测到 Docker Engine 未运行,请先启动 Docker Desktop
pause
exit /b 1
)
REM 3) 构建镜像并启动容器(使用 compose)
echo 🏗️ 正在构建镜像...
docker compose build
if errorlevel 1 (
echo ❌ 构建失败
pause
exit /b 1
)
echo 🚀 正在启动容器...
docker compose up -d
if errorlevel 1 (
echo ❌ 启动失败
pause
exit /b 1
)
echo ✅ 启动完成,请访问: http://localhost:8080/
echo 如需停止: docker compose down
pause
@echo off
setlocal ENABLEDELAYEDEXPANSION
set PORT=8080
cd /d "%~dp0"
echo ========================================
echo Start static server (port %PORT%)
echo ========================================
REM [1/3] Free port using PowerShell only
powershell -NoProfile -ExecutionPolicy Bypass -Command "$port=%PORT%; try { $cons=Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue; if ($cons) { ($cons | Select-Object -ExpandProperty OwningProcess -Unique) | ForEach-Object { try { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue } catch {} } } } catch {}"
REM Small wait
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Sleep -Seconds 1"
REM [2/3] Check Node.js using PowerShell
powershell -NoProfile -ExecutionPolicy Bypass -Command "if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Host 'Node.js not found. Please install from https://nodejs.org/'; exit 1 }"
if errorlevel 1 (
pause
exit /b 1
)
REM [3/3] Start server (with SPA fallback)
echo Starting server: http://localhost:%PORT%/
powershell -NoProfile -ExecutionPolicy Bypass -Command "node serve-dist.js"
echo Server stopped.
pause
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm',
'.vue': 'text/plain',
'.md': 'text/markdown'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 解析URL
const parsedUrl = url.parse(req.url);
let pathname = parsedUrl.pathname;
// 如果是根路径,重定向到index.html
if (pathname === '/') {
pathname = '/index.html';
}
// 构建文件路径
const filePath = path.join(__dirname, pathname);
// 检查文件是否存在
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
// 文件不存在
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<head>
<title>404 - 文件未找到</title>
<meta charset="utf-8">
</head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1 style="color: #f56c6c;">404 - 文件未找到</h1>
<p>请求的文件 ${pathname} 不存在</p>
<p><a href="/" style="color: #409eff;">返回首页</a></p>
</body>
</html>
`);
return;
}
// 获取文件扩展名
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
// 读取文件
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<head>
<title>500 - 服务器错误</title>
<meta charset="utf-8">
</head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1 style="color: #f56c6c;">500 - 服务器错误</h1>
<p>读取文件时发生错误: ${err.message}</p>
</body>
</html>
`);
return;
}
// 设置CORS头部
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 设置缓存控制
if (ext === '.js' || ext === '.css') {
res.setHeader('Cache-Control', 'no-cache');
}
// 返回文件内容
res.writeHead(200, { 'Content-Type': contentType + '; charset=utf-8' });
res.end(data);
});
});
});
// 尝试不同的端口
const ports = [5174, 5175, 5176, 8080, 3000];
let currentPortIndex = 0;
function tryStartServer() {
const PORT = ports[currentPortIndex];
const HOST = '127.0.0.1';
const serverInstance = server.listen(PORT, HOST, () => {
console.log('========================================');
console.log('🚀 绩效计分系统 v8.6 优化版本服务器启动成功!');
console.log('========================================');
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主页面: http://localhost:${PORT}/index.html`);
console.log(`🧪 测试脚本: http://localhost:${PORT}/test-v86-optimizations.js`);
console.log(`📋 版本说明: http://localhost:${PORT}/v8.6优化版本说明.md`);
console.log('');
console.log('✨ v8.6 优化功能已启用:');
console.log(' • 智能图片压缩 (减少50-70%存储)');
console.log(' • 实时存储监控');
console.log(' • 自动备份恢复');
console.log(' • 增强用户体验');
console.log(' • 数据分离优化');
console.log('');
console.log('🔧 使用说明:');
console.log(' 1. 在浏览器中打开上述地址');
console.log(' 2. 使用 admin/admin123 登录管理员账户');
console.log(' 3. 查看"数据统计"标签的数据分离状态');
console.log(' 4. 测试"数据管理"标签的备份功能');
console.log(' 5. 体验优化后的图片上传功能');
console.log('');
console.log('按 Ctrl+C 停止服务器');
console.log('========================================');
// 自动打开浏览器
const { exec } = require('child_process');
exec(`start "" "http://localhost:${PORT}/index.html"`, (error) => {
if (error) {
console.log('💡 请手动在浏览器中打开: http://localhost:' + PORT);
}
});
});
serverInstance.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`⚠️ 端口 ${PORT} 被占用,尝试下一个端口...`);
currentPortIndex++;
if (currentPortIndex < ports.length) {
tryStartServer();
} else {
console.log('❌ 所有端口都被占用,请手动指定端口或关闭占用端口的程序');
process.exit(1);
}
} else {
console.error('❌ 服务器启动失败:', err.message);
process.exit(1);
}
});
}
// 启动服务器
tryStartServer();
// 优雅关闭
process.on('SIGINT', () => {
console.log('\n🛑 正在关闭服务器...');
server.close(() => {
console.log('✅ 服务器已关闭');
process.exit(0);
});
});
process.on('SIGTERM', () => {
console.log('\n🛑 收到终止信号,正在关闭服务器...');
server.close(() => {
console.log('✅ 服务器已关闭');
process.exit(0);
});
});
# 绩效计分系统启动脚本
# 用于启动前端服务器和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
}
@echo off
chcp 65001 >nul
echo ========================================
echo 绩效计分系统 v8.6 - 开发服务器
echo ========================================
echo.
echo 🚀 正在启动开发服务器...
echo.
echo 启动成功后,请在浏览器中访问: http://localhost:8080
echo.
echo 默认登录账号:
echo - 管理员: admin / admin123
echo - 陈锐屏: 13800138001 / 123456
echo - 张田田: 13800138002 / 123456
echo - 余芳飞: 13800138003 / 123456
echo.
echo 按 Ctrl+C 可停止服务器
echo ========================================
echo.
node simple-dev-server.cjs
pause
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() 来运行所有测试')
}
// 测试图片相关问题的修复效果
console.log('=== 测试图片相关问题修复效果 ===');
// 检查是否在浏览器环境中
if (typeof window === 'undefined' || !window.dataStore) {
console.error('❌ 请在浏览器控制台中运行此脚本');
throw new Error('需要在浏览器环境中运行');
}
const dataStore = window.dataStore;
// 1. 执行诊断脚本
console.log('\n1️⃣ 执行数据诊断...');
try {
// 运行诊断脚本
eval(document.querySelector('script[src*="diagnose-image-display-issue.js"]')?.textContent || '');
} catch (error) {
console.log('诊断脚本未找到,手动执行诊断...');
// 手动诊断
const usersData = JSON.parse(localStorage.getItem('score_system_users') || '[]');
const institutionsData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]');
console.log('用户总数:', usersData.length);
console.log('机构总数:', institutionsData.length);
const zhangTianTian = usersData.find(user => user.name === '张田田');
if (zhangTianTian) {
const zhangInstitutions = institutionsData.filter(inst => inst.ownerId === zhangTianTian.id);
console.log('张田田负责的机构数量:', zhangInstitutions.length);
zhangInstitutions.forEach(inst => {
console.log(` - ${inst.name} (图片数量: ${inst.images ? inst.images.length : 0})`);
});
}
}
// 2. 执行修复
console.log('\n2️⃣ 执行综合修复...');
const fixResults = dataStore.fixAllImageIssues();
console.log('修复结果:', fixResults);
// 3. 验证修复效果
console.log('\n3️⃣ 验证修复效果...');
// 检查张田田的机构数据
const zhangTianTian = dataStore.getUsers().find(user => user.name === '张田田');
if (zhangTianTian) {
console.log('张田田用户信息:');
console.log(' ID:', zhangTianTian.id);
const zhangInstitutions = dataStore.getInstitutionsByUserId(zhangTianTian.id);
console.log(' 负责机构数量:', zhangInstitutions.length);
zhangInstitutions.forEach(inst => {
console.log(` - ${inst.name}`);
console.log(` 内部ID: ${inst.id}`);
console.log(` 机构编号: ${inst.institutionId}`);
console.log(` 图片数量: ${inst.images ? inst.images.length : 0}`);
if (inst.images && inst.images.length > 0) {
inst.images.forEach((img, index) => {
console.log(` 图片${index + 1}: ${img.name} (ID: ${img.id})`);
});
}
});
// 检查武夷山思美达口腔门诊部
const wuyishanInst = zhangInstitutions.find(inst =>
inst.name.includes('武夷山思美达') || inst.name.includes('思美达')
);
if (wuyishanInst) {
console.log('\n武夷山思美达口腔门诊部状态:');
console.log(' ✅ 归属正确,属于张田田');
console.log(' 图片数量:', wuyishanInst.images.length);
} else {
console.log('\n❌ 未找到武夷山思美达口腔门诊部或归属错误');
}
} else {
console.log('❌ 未找到用户张田田');
}
// 4. 测试图片上传权限
console.log('\n4️⃣ 测试图片上传权限...');
if (zhangTianTian) {
const testResults = dataStore.testImageUploadPermissions(zhangTianTian.id);
console.log('权限测试结果:', testResults);
}
// 5. 检查数据一致性
console.log('\n5️⃣ 检查数据一致性...');
const integrityCheck = dataStore.comprehensiveDataIntegrityCheck();
console.log('数据完整性检查结果:', integrityCheck);
// 6. 提供测试建议
console.log('\n6️⃣ 测试建议:');
console.log('1. 尝试以张田田身份登录系统');
console.log('2. 找到武夷山思美达口腔门诊部');
console.log('3. 尝试上传图片,验证是否还会显示在其他机构');
console.log('4. 尝试删除图片,验证删除功能是否正常');
console.log('5. 检查图片是否正确显示在对应机构中');
console.log('\n=== 测试完成 ===');
// 返回测试结果摘要
const testSummary = {
fixResults: fixResults,
zhangTianTianFound: !!zhangTianTian,
zhangInstitutionsCount: zhangTianTian ? dataStore.getInstitutionsByUserId(zhangTianTian.id).length : 0,
integrityCheck: integrityCheck,
timestamp: new Date().toISOString()
};
console.log('\n📊 测试结果摘要:', testSummary);
return testSummary;
/**
* 测试新功能的脚本
* 包括重复图片检测和历史统计功能
*/
// 在浏览器控制台中运行此脚本
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() - 清理测试数据')
}
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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