Commit df8a7f8d by Performance System

🚀 Release v8.8.0: 图片上传修复 + Docker部署支持

 新功能:
- 🐳 新增 Docker 容器化部署支持
- 🛠️ 添加一键部署和管理脚本
- 📋 完整的 Docker 部署文档

🔧 核心修复:
- 🖼️ 修复图片上传归属错乱问题
- 🗑️ 修复图片删除后界面不更新问题
- 🔒 强化权限验证,防止跨机构操作
- 🧰 数据加载时自动修复机构ID冲突

️ 启动优化:
- 🔄 升级 start-server.bat 支持自动端口释放
- 🌐 启用 SPA 路由回退,解决刷新404问题
- 📡 使用 Node.js 静态服务器替代 Python

🏗️ 技术改进:
- 📦 多阶段 Docker 构建优化
- 🔧 Nginx 配置支持前端路由
- 🛡️ 增强的错误处理和日志记录
- 📊 容器健康检查机制

📁 新增文件:
- Dockerfile, docker-compose.yml
- nginx.conf (SPA 回退配置)
- docker-deploy.bat, docker-manage.bat
- serve-dist.js (Node 静态服务器)
- 诊断和测试脚本

🎯 部署方式:
- 本地: npm run build + node serve-dist.js
- Docker: docker-compose up -d
- 访问: http://localhost:3000/

版本: 8.8.0
构建时间: 2025-08-08
parent 7d56d89d
# 8.8 版本 Dockerfile - 生产镜像(多阶段构建)
# 1) Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# 只拷贝必要文件以提高缓存命中率
COPY package.json package-lock.json* ./
RUN npm ci --silent || npm install --legacy-peer-deps
# 拷贝源码并构建
COPY . .
RUN npm run build
# 2) Runtime stage (Nginx)
FROM nginx:alpine
# 安装 bash 以便容器内调试(可选)
RUN apk add --no-cache bash
# 拷贝构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 拷贝 Nginx 配置(启用前端路由回退)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 健康检查(可选)
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost/ || exit 1
# 启动 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)
### 重要变更
- 🖼️ 修复图片上传归属错乱与删除后不更新问题
- 🔒 强化权限校验:上传/删除仅限所属机构
- ⚙️ 启动脚本升级:一键释放端口并启用 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=== 诊断完成 ===');
version: "3.8"
services:
scoring-app:
build:
context: .
dockerfile: Dockerfile
container_name: scoring-app-8.8
ports:
- "8080:80"
restart: unless-stopped
environment:
- 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
# 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";
}
}
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}`);
}
});
......@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = 5174;
const PORT = 8080;
// MIME类型映射
const mimeTypes = {
......
......@@ -518,7 +518,12 @@ const handleImageUpload = (uploadFile, institutionId) => {
}
}
const result = dataStore.addImageToInstitution(institutionId, imageData, authStore.currentUser.id)
const result = dataStore.addImageToInstitution(
institutionId,
imageData,
authStore.currentUser.id,
{ expectedName: institution.name, expectedInstitutionId: institution.institutionId }
)
if (result) {
console.log('图片添加成功:', result)
......@@ -571,25 +576,41 @@ const removeImage = async (institutionId, imageId) => {
})
console.log('开始删除图片:', { institutionId, imageId })
const success = dataStore.removeImageFromInstitution(institutionId, imageId)
// 传递当前用户ID进行权限验证
const success = dataStore.removeImageFromInstitution(institutionId, imageId, authStore.currentUser.id)
if (success) {
console.log('图片删除成功')
ElMessage.success('图片删除成功!')
// 强制刷新数据(只从用户机构中查找)
// 强制刷新数据确保界面更新
nextTick(() => {
// 重新加载数据以确保界面同步
dataStore.loadFromStorage()
// 验证删除结果
const userInstitutions = dataStore.getInstitutionsByUserId(authStore.currentUser.id)
const institution = userInstitutions.find(inst => inst.id === institutionId)
console.log('删除后机构图片数量:', institution?.images.length)
console.log('删除后机构图片数量:', institution?.images.length || 0)
// 验证localStorage中的数据
const savedData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]')
const savedInstitution = savedData.find(inst => inst.id === institutionId)
console.log('localStorage中的图片数量:', savedInstitution?.images?.length || 0)
})
}
} catch (error) {
if (error.message && error.message.includes('🚨')) {
// 安全错误
ElMessage.error(error.message)
} else if (error.message) {
// 其他错误
ElMessage.error(`删除失败: ${error.message}`)
} else {
console.error('图片删除失败')
ElMessage.error('图片删除失败!')
// 用户取消删除
console.log('用户取消删除图片')
}
} catch {
// 用户取消删除
console.log('用户取消删除图片')
}
}
......
@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
echo 启动v8.7开发服务器...
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 ❌ 错误: 未找到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安装
echo 错误: 未找到npm,请检查Node.js安装
pause
exit /b 1
)
echo ✅ npm 环境检查通过
npm --version
echo.
REM 检查node_modules是否存在
if not exist "node_modules" (
echo 正在安装依赖...
echo 📦 正在安装项目依赖...
echo 这可能需要几分钟时间,请耐心等待...
npm install
if errorlevel 1 (
echo 错误: 依赖安装失败
echo ❌ 依赖安装失败,请检查网络连接
pause
exit /b 1
)
echo ✅ 依赖安装完成
)
echo 启动绩效计分系统 v8.7...
echo 服务器将在 http://localhost:5174 启动
echo 主系统: http://localhost:5174/index.html
echo 测试页面: http://localhost:5174/public/test-v87.html
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 5174 --host
REM 尝试启动vite开发服务器,使用不同端口避免权限问题
echo 正在启动vite开发服务器...
npx vite --port 8080 --host 127.0.0.1
REM 如果vite失败,使用简单HTTP服务器
if errorlevel 1 (
echo.
echo vite启动失败,使用简单HTTP服务器...
node simple-server.js
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
@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('=== 测试图片相关问题修复效果 ===');
// 检查是否在浏览器环境中
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 testUploadFix() {
console.log('🧪 开始测试图片上传权限修复效果...')
// 1. 检查数据存储是否可用
if (typeof window === 'undefined' || !window.dataStore) {
console.error('❌ 请在浏览器中运行此脚本,并确保dataStore已加载')
return false
}
const dataStore = window.dataStore
// 2. 执行修复函数
console.log('\n1️⃣ 执行图片上传权限修复...')
try {
const fixResult = dataStore.fixImageUploadPermissionErrors()
console.log('修复结果:', fixResult)
if (fixResult.fixed > 0) {
console.log(`✅ 修复了 ${fixResult.fixed} 个问题`)
fixResult.issues.forEach((issue, index) => {
console.log(` ${index + 1}. ${issue}`)
})
} else {
console.log('✅ 未发现需要修复的问题')
}
} catch (error) {
console.error('❌ 修复过程中出错:', error)
return false
}
// 3. 测试张田田的权限
console.log('\n2️⃣ 测试张田田的上传权限...')
// 获取张田田的用户信息
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]')
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]')
const zhangTianTian = users.find(u => u.name === '张田田')
if (!zhangTianTian) {
console.error('❌ 未找到用户张田田')
return false
}
console.log('张田田用户信息:', {
id: zhangTianTian.id,
name: zhangTianTian.name,
role: zhangTianTian.role
})
// 查找张田田负责的机构
const zhangInstitutions = institutions.filter(inst => inst.ownerId === zhangTianTian.id)
console.log(`张田田负责的机构数量: ${zhangInstitutions.length}`)
zhangInstitutions.forEach((inst, index) => {
console.log(` ${index + 1}. ${inst.name}`)
console.log(` 内部ID: ${inst.id}`)
console.log(` 机构编号: ${inst.institutionId}`)
console.log(` 图片数量: ${inst.images ? inst.images.length : 0}`)
})
// 4. 检查武夷山思美达口腔门诊部
console.log('\n3️⃣ 检查武夷山思美达口腔门诊部...')
const wuyishanInst = institutions.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)
const owner = users.find(u => u.id === wuyishanInst.ownerId)
console.log(' 负责人:', owner ? owner.name : '未找到')
if (wuyishanInst.ownerId === zhangTianTian.id) {
console.log(' ✅ 权限正确: 张田田可以操作此机构')
} else {
console.log(' ❌ 权限错误: 张田田无法操作此机构')
}
} else {
console.log('❌ 未找到武夷山思美达口腔门诊部')
}
// 5. 模拟图片上传测试
console.log('\n4️⃣ 模拟图片上传测试...')
if (zhangInstitutions.length > 0) {
const testInstitution = zhangInstitutions[0]
console.log(`测试上传到机构: ${testInstitution.name}`)
// 模拟图片数据
const mockImageData = {
name: '测试图片.jpg',
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=',
size: 1024,
uploadTime: new Date().toISOString()
}
try {
// 测试权限验证逻辑(不实际添加图片)
console.log('开始权限验证测试...')
// 检查用户是否存在
if (!zhangTianTian.id) {
throw new Error('用户ID不存在')
}
// 检查机构是否在用户权限范围内
const userInstitutions = institutions.filter(inst => inst.ownerId === zhangTianTian.id)
const targetInstitution = userInstitutions.find(inst => inst.id === testInstitution.id)
if (!targetInstitution) {
throw new Error('机构不在用户权限范围内')
}
console.log('✅ 权限验证测试通过')
console.log('可以正常上传图片到:', targetInstitution.name)
// 实际测试上传(如果需要)
if (confirm('是否要实际测试图片上传?(会添加一张测试图片)')) {
try {
const result = dataStore.addImageToInstitution(
testInstitution.id,
mockImageData,
zhangTianTian.id
)
if (result) {
console.log('✅ 图片上传测试成功!')
console.log('上传结果:', result)
// 清理测试图片
if (confirm('是否要删除测试图片?')) {
const deleteResult = dataStore.removeImageFromInstitution(testInstitution.id, result.id)
if (deleteResult) {
console.log('✅ 测试图片已删除')
}
}
} else {
console.error('❌ 图片上传测试失败')
}
} catch (uploadError) {
console.error('❌ 图片上传测试出错:', uploadError.message)
}
}
} catch (error) {
console.error('❌ 权限验证测试失败:', error.message)
}
} else {
console.log('❌ 张田田没有负责任何机构,无法进行上传测试')
}
// 6. 生成测试报告
console.log('\n5️⃣ 测试报告...')
const report = {
timestamp: new Date().toISOString(),
user: zhangTianTian ? zhangTianTian.name : '未找到',
userInstitutions: zhangInstitutions.length,
wuyishanFound: !!wuyishanInst,
wuyishanOwner: wuyishanInst ? (users.find(u => u.id === wuyishanInst.ownerId)?.name || '未知') : null,
permissionCorrect: wuyishanInst ? (wuyishanInst.ownerId === zhangTianTian?.id) : false
}
console.log('测试报告:', report)
if (report.permissionCorrect) {
console.log('🎉 修复成功!张田田现在可以正常上传图片到武夷山思美达口腔门诊部')
} else {
console.log('⚠️ 仍存在问题,需要进一步检查')
}
return report
}
// 快速诊断函数
function quickDiagnose() {
console.log('🔍 快速诊断图片上传问题...')
const users = JSON.parse(localStorage.getItem('score_system_users') || '[]')
const institutions = JSON.parse(localStorage.getItem('score_system_institutions') || '[]')
const zhangTianTian = users.find(u => u.name === '张田田')
const wuyishanInst = institutions.find(inst =>
inst.name.includes('武夷山思美达') || inst.name.includes('思美达')
)
const chongchuanInst = institutions.find(inst =>
inst.name.includes('崇川区海虹') || inst.name.includes('海虹')
)
console.log('诊断结果:')
console.log('张田田用户:', zhangTianTian ? '✅ 存在' : '❌ 不存在')
console.log('武夷山思美达机构:', wuyishanInst ? '✅ 存在' : '❌ 不存在')
console.log('崇川区海虹机构:', chongchuanInst ? '✅ 存在' : '❌ 不存在')
if (zhangTianTian && wuyishanInst) {
console.log('武夷山思美达归属:', wuyishanInst.ownerId === zhangTianTian.id ? '✅ 正确' : '❌ 错误')
}
if (zhangTianTian && chongchuanInst) {
console.log('崇川区海虹归属:', chongchuanInst.ownerId === zhangTianTian.id ? '⚠️ 错误归属' : '✅ 正确')
}
}
// 导出函数供浏览器使用
if (typeof window !== 'undefined') {
window.testUploadFix = testUploadFix
window.quickDiagnose = quickDiagnose
console.log('测试脚本已加载!')
console.log('使用方法:')
console.log('1. testUploadFix() - 完整测试修复效果')
console.log('2. quickDiagnose() - 快速诊断问题')
}
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