Commit ccdb2e26 by Performance System

update

parent e0413fc4
# 前端环境变量配置
# API 服务地址
VITE_API_BASE_URL=http://localhost:8000
# 应用配置
VITE_APP_TITLE=绩效计分系统
VITE_APP_VERSION=8.8.0
# 开发模式配置
VITE_DEV_MODE=true
...@@ -8,8 +8,14 @@ WORKDIR /app ...@@ -8,8 +8,14 @@ WORKDIR /app
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
RUN npm ci --silent || npm install --legacy-peer-deps RUN npm ci --silent || npm install --legacy-peer-deps
# 拷贝源码并构建 # 拷贝源码
COPY . . COPY . .
# 设置构建时环境变量
ARG VITE_API_BASE_URL=http://localhost:8000
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
# 构建应用
RUN npm run build RUN npm run build
# 2) Runtime stage (Nginx) # 2) Runtime stage (Nginx)
......
...@@ -106,9 +106,7 @@ npm run preview ...@@ -106,9 +106,7 @@ npm run preview
│ │ └── theme.css │ │ └── theme.css
│ ├── utils/ # 工具函数 │ ├── utils/ # 工具函数
│ │ ├── dataCleanup.js │ │ ├── dataCleanup.js
│ │ ├── index.js │ │ └── index.js
│ │ ├── serverDataSync.js
│ │ └── simpleCrossBrowserSync.js
│ ├── App.vue # 根组件 │ ├── App.vue # 根组件
│ └── main.js # 入口文件 │ └── main.js # 入口文件
├── docs/ # 项目文档 ├── docs/ # 项目文档
......
# 数据库配置
DATABASE_URL=postgresql://performance_user:performance_pass@localhost:5432/performance_db
# API 服务配置
API_HOST=0.0.0.0
API_PORT=8000
# CORS 配置(多个域名用逗号分隔)
CORS_ORIGINS=http://localhost:5173,http://localhost:8080,http://localhost:4001
# 安全配置
SECRET_KEY=your-secret-key-here-change-in-production
# 日志配置
LOG_LEVEL=INFO
LOG_FILE=logs/api.log
# 数据迁移配置
MIGRATION_BATCH_SIZE=100
# FastAPI 后端服务 Dockerfile
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
curl \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 创建日志目录
RUN mkdir -p logs
# 暴露端口
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
"""
JWT 认证工具模块
提供 JWT token 生成、验证、刷新等功能
以及密码加密和验证功能
"""
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, status
from loguru import logger
from config import settings
# 密码加密上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class JWTManager:
"""JWT 管理器"""
def __init__(self):
self.secret_key = settings.JWT_SECRET_KEY
self.algorithm = settings.JWT_ALGORITHM
self.access_token_expire_hours = settings.JWT_EXPIRE_HOURS
self.refresh_token_expire_days = 7 # 刷新token有效期7天
def create_access_token(self, data: Dict[str, Any]) -> str:
"""
创建访问token
Args:
data: 要编码到token中的数据
Returns:
JWT access token字符串
"""
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(hours=self.access_token_expire_hours)
to_encode.update({
"exp": expire,
"type": "access"
})
try:
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
logger.info(f"创建访问token成功,用户: {data.get('sub')}")
return encoded_jwt
except Exception as e:
logger.error(f"创建访问token失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token创建失败"
)
def create_refresh_token(self, data: Dict[str, Any]) -> str:
"""
创建刷新token
Args:
data: 要编码到token中的数据
Returns:
JWT refresh token字符串
"""
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(days=self.refresh_token_expire_days)
to_encode.update({
"exp": expire,
"type": "refresh"
})
try:
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
logger.info(f"创建刷新token成功,用户: {data.get('sub')}")
return encoded_jwt
except Exception as e:
logger.error(f"创建刷新token失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="刷新Token创建失败"
)
def verify_token(self, token: str, token_type: str = "access") -> Dict[str, Any]:
"""
验证token
Args:
token: JWT token字符串
token_type: token类型 ("access" 或 "refresh")
Returns:
解码后的token数据
Raises:
HTTPException: token无效时抛出异常
"""
try:
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# 检查token类型
if payload.get("type") != token_type:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"无效的token类型,期望: {token_type}"
)
# 检查过期时间
exp = payload.get("exp")
if exp is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token缺少过期时间"
)
if datetime.now(timezone.utc) > datetime.fromtimestamp(exp, timezone.utc):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token已过期"
)
return payload
except JWTError as e:
logger.warning(f"Token验证失败: {e}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的token"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Token验证异常: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token验证失败"
)
def refresh_access_token(self, refresh_token: str) -> str:
"""
使用刷新token生成新的访问token
Args:
refresh_token: 刷新token字符串
Returns:
新的访问token
"""
# 验证刷新token
payload = self.verify_token(refresh_token, "refresh")
# 创建新的访问token数据
new_data = {
"sub": payload.get("sub"),
"user_id": payload.get("user_id"),
"role": payload.get("role"),
"phone": payload.get("phone")
}
return self.create_access_token(new_data)
class PasswordManager:
"""密码管理器"""
@staticmethod
def hash_password(password: str) -> str:
"""
加密密码
Args:
password: 明文密码
Returns:
加密后的密码哈希
"""
try:
hashed = pwd_context.hash(password)
logger.info("密码加密成功")
return hashed
except Exception as e:
logger.error(f"密码加密失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="密码加密失败"
)
@staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
验证密码
Args:
plain_password: 明文密码
hashed_password: 加密后的密码哈希
Returns:
密码是否匹配
"""
try:
result = pwd_context.verify(plain_password, hashed_password)
logger.info(f"密码验证结果: {'成功' if result else '失败'}")
return result
except Exception as e:
logger.error(f"密码验证异常: {e}")
return False
@staticmethod
def is_hashed_password(password: str) -> bool:
"""
检查密码是否已经是哈希格式
Args:
password: 密码字符串
Returns:
是否为哈希密码
"""
# bcrypt哈希通常以$2b$开头,长度为60字符
return password.startswith("$2b$") and len(password) == 60
# 创建全局实例
jwt_manager = JWTManager()
password_manager = PasswordManager()
"""
应用配置模块
使用 Pydantic Settings 管理配置参数
"""
from pydantic_settings import BaseSettings
from typing import List
import os
class Settings(BaseSettings):
"""应用配置类"""
# 应用基本信息
APP_NAME: str = "绩效计分系统API"
APP_VERSION: str = "1.0.0"
DEBUG: bool = True
# 服务器配置
HOST: str = "0.0.0.0"
PORT: int = 8000
API_HOST: str = "0.0.0.0" # 兼容性字段
API_PORT: int = 8000 # 兼容性字段
# 数据库配置
DATABASE_URL: str = "postgresql://performance_user:performance_pass@localhost:5432/performance_db"
# JWT 配置
JWT_SECRET_KEY: str = "local-development-secret-key-not-for-production"
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_HOURS: int = 24
SECRET_KEY: str = "your-secret-key-here-change-in-production" # 兼容性字段
# 文件上传配置
UPLOAD_DIR: str = "uploads"
MAX_FILE_SIZE: int = 5242880 # 5MB
MAX_IMAGES_PER_INSTITUTION: int = 10
# CORS 配置
CORS_ORIGINS: List[str] = [
"http://localhost:5173", # Vite 开发服务器
"http://localhost:3000", # React 开发服务器
"http://localhost:4001", # Docker 前端
"http://localhost:4003", # 其他前端
"http://localhost:8080", # 生产环境前端
"http://127.0.0.1:3000",
"http://127.0.0.1:4001",
"http://127.0.0.1:4003",
"http://127.0.0.1:8080",
]
# WebSocket 配置
WEBSOCKET_HEARTBEAT_INTERVAL: int = 30
WEBSOCKET_TIMEOUT: int = 60
# 系统配置
ENABLE_REAL_TIME_SYNC: bool = True
SYNC_EVENT_RETENTION_DAYS: int = 30
SESSION_CLEANUP_INTERVAL: int = 3600
# 日志配置
LOG_LEVEL: str = "INFO"
LOG_FILE: str = "logs/api.log"
# 数据迁移配置
MIGRATION_BATCH_SIZE: int = 100
class Config:
env_file = ".env"
case_sensitive = True
extra = "ignore" # 忽略额外的环境变量
# 创建全局配置实例
settings = Settings()
"""
数据库连接和操作层
使用 databases 和 SQLAlchemy 进行异步数据库操作
"""
import databases
import sqlalchemy
from sqlalchemy import create_engine, MetaData, Table, Column, String, Integer, Text, TIMESTAMP, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from config import settings
from loguru import logger
# 创建数据库连接
database = databases.Database(settings.DATABASE_URL)
# 创建 SQLAlchemy 引擎
engine = create_engine(settings.DATABASE_URL)
# 创建元数据对象
metadata = MetaData()
# 定义用户表
users_table = Table(
"users",
metadata,
Column("id", String(50), primary_key=True),
Column("phone", String(20), unique=True, nullable=False),
Column("password", String(255), nullable=False),
Column("name", String(100), nullable=False),
Column("role", String(20), default="user"),
Column("institutions", JSONB, default="[]"),
Column("created_at", TIMESTAMP, server_default=func.now()),
Column("updated_at", TIMESTAMP, server_default=func.now(), onupdate=func.now()),
)
# 定义机构表
institutions_table = Table(
"institutions",
metadata,
Column("id", String(50), primary_key=True),
Column("institution_id", String(50), unique=True),
Column("name", String(200), nullable=False),
Column("owner_id", String(50), ForeignKey("users.id", ondelete="SET NULL")),
Column("created_at", TIMESTAMP, server_default=func.now()),
Column("updated_at", TIMESTAMP, server_default=func.now(), onupdate=func.now()),
)
# 定义机构图片表
institution_images_table = Table(
"institution_images",
metadata,
Column("id", String(50), primary_key=True),
Column("institution_id", String(50), ForeignKey("institutions.id", ondelete="CASCADE"), nullable=False),
Column("url", Text, nullable=False),
Column("upload_time", TIMESTAMP, nullable=False),
Column("created_at", TIMESTAMP, server_default=func.now()),
)
# 定义系统配置表
system_config_table = Table(
"system_config",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("config_key", String(100), unique=True, nullable=False),
Column("config_value", JSONB),
Column("description", Text),
Column("created_at", TIMESTAMP, server_default=func.now()),
Column("updated_at", TIMESTAMP, server_default=func.now(), onupdate=func.now()),
)
# 定义月度历史统计表
monthly_history_table = Table(
"monthly_history",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("month", String(7), unique=True, nullable=False),
Column("save_time", TIMESTAMP(timezone=True), nullable=False),
Column("total_users", Integer, nullable=False),
Column("total_institutions", Integer, nullable=False),
Column("total_images", Integer, nullable=False),
Column("user_stats", JSONB, nullable=False),
Column("created_at", TIMESTAMP(timezone=True), server_default=func.now()),
)
class DatabaseManager:
"""数据库管理器,提供通用的数据库操作方法"""
def __init__(self):
self.database = database
async def fetch_one(self, query, values=None):
"""执行查询并返回单条记录"""
try:
return await self.database.fetch_one(query, values)
except Exception as e:
logger.error(f"数据库查询失败: {e}")
raise
async def fetch_all(self, query, values=None):
"""执行查询并返回所有记录"""
try:
return await self.database.fetch_all(query, values)
except Exception as e:
logger.error(f"数据库查询失败: {e}")
raise
async def execute(self, query, values=None):
"""执行 INSERT/UPDATE/DELETE 操作"""
try:
return await self.database.execute(query, values)
except Exception as e:
logger.error(f"数据库操作失败: {e}")
raise
async def execute_many(self, query, values):
"""批量执行操作"""
try:
return await self.database.execute_many(query, values)
except Exception as e:
logger.error(f"批量数据库操作失败: {e}")
raise
async def transaction(self):
"""创建数据库事务"""
return self.database.transaction()
# 创建全局数据库管理器实例
db_manager = DatabaseManager()
async def get_database():
"""依赖注入:获取数据库连接"""
return db_manager
async def check_database_connection():
"""检查数据库连接状态"""
try:
await database.fetch_one("SELECT 1")
return True
except Exception as e:
logger.error(f"数据库连接检查失败: {e}")
return False
async def initialize_database():
"""初始化数据库表结构"""
try:
# 创建所有表
metadata.create_all(engine)
logger.info("✅ 数据库表结构初始化完成")
# 检查并插入默认数据
await insert_default_data()
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
raise
async def insert_default_data():
"""插入默认数据"""
try:
# 检查是否已有管理员用户
admin_exists = await database.fetch_one(
users_table.select().where(users_table.c.id == "admin")
)
if not admin_exists:
# 插入默认管理员用户
await database.execute(
users_table.insert().values(
id="admin",
phone="admin",
password="admin123",
name="系统管理员",
role="admin",
institutions=[]
)
)
logger.info("✅ 默认管理员用户创建成功")
# 检查并插入默认系统配置
config_items = [
("initialized", True, "系统是否已初始化"),
("version", "8.8.0", "系统版本"),
("hasDefaultData", False, "是否有默认示例数据")
]
for key, value, desc in config_items:
exists = await database.fetch_one(
system_config_table.select().where(system_config_table.c.config_key == key)
)
if not exists:
await database.execute(
system_config_table.insert().values(
config_key=key,
config_value=value,
description=desc
)
)
logger.info("✅ 默认系统配置创建成功")
except Exception as e:
logger.error(f"插入默认数据失败: {e}")
raise
"""
认证依赖注入模块
提供JWT认证、权限验证等依赖注入函数
"""
from typing import Optional, List
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import select
from loguru import logger
from auth import jwt_manager
from database import get_database, DatabaseManager, users_table
from models import UserResponse
# HTTP Bearer token 安全方案
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: DatabaseManager = Depends(get_database)
) -> UserResponse:
"""
获取当前认证用户
Args:
credentials: HTTP Bearer token凭据
db: 数据库管理器
Returns:
当前用户信息
Raises:
HTTPException: 认证失败时抛出异常
"""
try:
# 验证token
payload = jwt_manager.verify_token(credentials.credentials, "access")
user_id = payload.get("user_id")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token中缺少用户ID"
)
# 从数据库获取用户信息
query = users_table.select().where(users_table.c.id == user_id)
user = await db.fetch_one(query)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在"
)
# 返回用户信息
return UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
role=user["role"],
institutions=user["institutions"] or [],
created_at=user["created_at"],
updated_at=user["updated_at"]
)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取当前用户失败: {e}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="认证失败"
)
async def get_current_active_user(
current_user: UserResponse = Depends(get_current_user)
) -> UserResponse:
"""
获取当前活跃用户(可扩展为检查用户状态)
Args:
current_user: 当前用户
Returns:
当前活跃用户信息
"""
# 这里可以添加用户状态检查逻辑
# 例如检查用户是否被禁用、是否需要重新验证等
return current_user
def require_roles(allowed_roles: List[str]):
"""
角色权限验证装饰器工厂
Args:
allowed_roles: 允许的角色列表
Returns:
依赖注入函数
"""
async def role_checker(
current_user: UserResponse = Depends(get_current_active_user)
) -> UserResponse:
"""
检查用户角色权限
Args:
current_user: 当前用户
Returns:
当前用户信息
Raises:
HTTPException: 权限不足时抛出异常
"""
if current_user.role not in allowed_roles:
logger.warning(f"用户 {current_user.name} (角色: {current_user.role}) 尝试访问需要角色 {allowed_roles} 的资源")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"权限不足,需要角色: {', '.join(allowed_roles)}"
)
return current_user
return role_checker
# 常用的角色权限依赖
require_admin = require_roles(["admin"])
require_user_or_admin = require_roles(["user", "admin"])
async def get_optional_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)),
db: DatabaseManager = Depends(get_database)
) -> Optional[UserResponse]:
"""
获取可选的当前用户(不强制要求认证)
Args:
credentials: 可选的HTTP Bearer token凭据
db: 数据库管理器
Returns:
当前用户信息或None
"""
if not credentials:
return None
try:
# 验证token
payload = jwt_manager.verify_token(credentials.credentials, "access")
user_id = payload.get("user_id")
if user_id is None:
return None
# 从数据库获取用户信息
query = users_table.select().where(users_table.c.id == user_id)
user = await db.fetch_one(query)
if not user:
return None
# 返回用户信息
return UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
role=user["role"],
institutions=user["institutions"] or [],
created_at=user["created_at"],
updated_at=user["updated_at"]
)
except Exception as e:
logger.warning(f"可选用户认证失败: {e}")
return None
class TokenBlacklist:
"""Token黑名单管理(简单内存实现,生产环境建议使用Redis)"""
def __init__(self):
self._blacklisted_tokens = set()
def add_token(self, token: str):
"""将token添加到黑名单"""
self._blacklisted_tokens.add(token)
logger.info("Token已添加到黑名单")
def is_blacklisted(self, token: str) -> bool:
"""检查token是否在黑名单中"""
return token in self._blacklisted_tokens
def clear_expired_tokens(self):
"""清理过期的token(这里简化处理,实际应该根据token过期时间清理)"""
# 在实际应用中,应该解析token获取过期时间,然后清理过期的token
pass
# 创建全局token黑名单实例
token_blacklist = TokenBlacklist()
async def verify_token_not_blacklisted(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> HTTPAuthorizationCredentials:
"""
验证token不在黑名单中
Args:
credentials: HTTP Bearer token凭据
Returns:
验证通过的凭据
Raises:
HTTPException: token在黑名单中时抛出异常
"""
if token_blacklist.is_blacklisted(credentials.credentials):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token已失效"
)
return credentials
"""
绩效计分系统 FastAPI 后端服务
主要功能:
1. 提供完整的 CRUD API 接口
2. 替换前端的 localStorage 存储
3. 实现数据持久化到 PostgreSQL
4. 支持数据迁移和同步
"""
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager
import os
from loguru import logger
from database import database, engine, metadata
from config import settings
from routers import users, institutions, system_config, history, migration
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时执行
logger.info("🚀 启动绩效计分系统 API 服务")
# 连接数据库
await database.connect()
logger.info("✅ 数据库连接成功")
# 创建表结构(如果不存在)
metadata.create_all(engine)
logger.info("✅ 数据库表结构检查完成")
yield
# 关闭时执行
logger.info("🔄 正在关闭 API 服务")
await database.disconnect()
logger.info("✅ 数据库连接已关闭")
# 创建 FastAPI 应用实例
app = FastAPI(
title="绩效计分系统 API",
description="为绩效计分系统提供数据持久化和 API 服务",
version="1.0.0",
lifespan=lifespan,
docs_url="/docs",
redoc_url="/redoc"
)
# 配置 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
# 注册路由
app.include_router(users.router, prefix="/api/users", tags=["用户管理"])
app.include_router(institutions.router, prefix="/api/institutions", tags=["机构管理"])
app.include_router(system_config.router, prefix="/api/config", tags=["系统配置"])
app.include_router(history.router, prefix="/api/history", tags=["历史数据"])
app.include_router(migration.router, prefix="/api/migration", tags=["数据迁移"])
@app.get("/", summary="根路径")
async def root():
"""API 根路径,返回服务信息"""
return {
"message": "绩效计分系统 API 服务",
"version": "1.0.0",
"status": "running",
"docs": "/docs"
}
@app.get("/health", summary="健康检查")
async def health_check():
"""健康检查端点,用于容器健康检查"""
try:
# 检查数据库连接
await database.fetch_one("SELECT 1")
from datetime import datetime
return {
"status": "healthy",
"database": "connected",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"健康检查失败: {e}")
raise HTTPException(status_code=503, detail="Service unavailable")
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
"""全局异常处理器"""
logger.error(f"未处理的异常: {exc}")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error", "error": str(exc)}
)
if __name__ == "__main__":
import uvicorn
# 配置日志
logger.add(
"logs/api.log",
rotation="1 day",
retention="30 days",
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)
# 启动服务
uvicorn.run(
"main:app",
host=settings.API_HOST,
port=settings.API_PORT,
reload=True if os.getenv("NODE_ENV") != "production" else False,
log_level="info"
)
"""
Pydantic 数据模型定义
用于 API 请求和响应的数据验证和序列化
"""
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Any, Dict
from datetime import datetime
import json
# 基础模型
class BaseResponse(BaseModel):
"""基础响应模型"""
success: bool = True
message: str = "操作成功"
data: Optional[Any] = None
class ErrorResponse(BaseModel):
"""错误响应模型"""
success: bool = False
message: str
error: Optional[str] = None
# 用户相关模型
class UserBase(BaseModel):
"""用户基础模型"""
phone: str = Field(..., min_length=1, max_length=20, description="手机号")
name: str = Field(..., min_length=1, max_length=100, description="用户姓名")
role: str = Field(default="user", description="用户角色")
institutions: List[str] = Field(default=[], description="负责的机构ID列表")
class UserCreate(UserBase):
"""创建用户模型"""
id: str = Field(..., min_length=1, max_length=50, description="用户ID")
password: str = Field(..., min_length=1, max_length=255, description="密码")
class UserUpdate(BaseModel):
"""更新用户模型"""
phone: Optional[str] = Field(None, min_length=1, max_length=20)
name: Optional[str] = Field(None, min_length=1, max_length=100)
password: Optional[str] = Field(None, min_length=1, max_length=255)
role: Optional[str] = None
institutions: Optional[List[str]] = None
class UserResponse(UserBase):
"""用户响应模型"""
id: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# 机构图片模型
class InstitutionImage(BaseModel):
"""机构图片模型"""
id: str = Field(..., description="图片ID")
url: str = Field(..., description="图片URL或Base64数据")
uploadTime: datetime = Field(..., description="上传时间")
class InstitutionImageCreate(BaseModel):
"""创建机构图片模型"""
id: str
url: str
upload_time: datetime
# 机构相关模型
class InstitutionBase(BaseModel):
"""机构基础模型"""
name: str = Field(..., min_length=1, max_length=200, description="机构名称")
institution_id: Optional[str] = Field(None, max_length=50, description="机构编号")
owner_id: Optional[str] = Field(None, max_length=50, description="负责人ID")
class InstitutionCreate(InstitutionBase):
"""创建机构模型"""
id: str = Field(..., min_length=1, max_length=50, description="机构ID")
class InstitutionUpdate(BaseModel):
"""更新机构模型"""
name: Optional[str] = Field(None, min_length=1, max_length=200)
institution_id: Optional[str] = Field(None, max_length=50)
owner_id: Optional[str] = Field(None, max_length=50)
class InstitutionBatchCreate(BaseModel):
"""批量创建机构模型"""
institutions: List[InstitutionCreate] = Field(..., description="机构列表")
class InstitutionBatchDelete(BaseModel):
"""批量删除机构模型"""
institution_ids: List[str] = Field(..., description="要删除的机构ID列表")
class InstitutionBatchResponse(BaseModel):
"""批量操作响应模型"""
success_count: int = Field(..., description="成功数量")
error_count: int = Field(..., description="失败数量")
errors: List[str] = Field(default=[], description="错误信息列表")
message: str = Field(..., description="操作结果消息")
class InstitutionBatchCreate(BaseModel):
"""批量创建机构模型"""
institutions: List[InstitutionCreate] = Field(..., description="机构列表")
class InstitutionBatchDelete(BaseModel):
"""批量删除机构模型"""
institution_ids: List[str] = Field(..., description="要删除的机构ID列表")
class InstitutionBatchResponse(BaseModel):
"""批量操作响应模型"""
success_count: int = Field(..., description="成功数量")
error_count: int = Field(..., description="失败数量")
errors: List[str] = Field(default=[], description="错误信息列表")
message: str = Field(..., description="操作结果消息")
class InstitutionResponse(InstitutionBase):
"""机构响应模型"""
id: str
images: List[InstitutionImage] = []
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# 系统配置模型
class SystemConfigItem(BaseModel):
"""系统配置项模型"""
config_key: str = Field(..., max_length=100, description="配置键名")
config_value: Any = Field(..., description="配置值")
description: Optional[str] = Field(None, description="配置描述")
class SystemConfigResponse(BaseModel):
"""系统配置响应模型"""
id: int
config_key: str
config_value: Any
description: Optional[str]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# 历史统计数据模型
class UserStatsItem(BaseModel):
"""用户统计项模型"""
userId: str
userName: str
institutionCount: int
interactionScore: float
performanceScore: float
institutions: List[Dict[str, Any]]
class MonthlyHistoryCreate(BaseModel):
"""创建月度历史记录模型"""
month: str = Field(..., pattern=r'^\d{4}-\d{2}$', description="月份,格式:YYYY-MM")
save_time: datetime = Field(..., description="保存时间")
total_users: int = Field(..., ge=0, description="总用户数")
total_institutions: int = Field(..., ge=0, description="总机构数")
total_images: int = Field(..., ge=0, description="总图片数")
user_stats: List[UserStatsItem] = Field(..., description="用户统计数据")
class MonthlyHistoryResponse(BaseModel):
"""月度历史记录响应模型"""
id: int
month: str
save_time: datetime
total_users: int
total_institutions: int
total_images: int
user_stats: List[UserStatsItem]
created_at: datetime
class Config:
from_attributes = True
# 数据迁移模型
class MigrationData(BaseModel):
"""数据迁移模型"""
users: List[Dict[str, Any]] = Field(..., description="用户数据")
institutions: List[Dict[str, Any]] = Field(..., description="机构数据")
systemConfig: Dict[str, Any] = Field(..., description="系统配置数据")
historyData: Optional[Dict[str, Any]] = Field(None, description="历史统计数据")
class MigrationResponse(BaseModel):
"""数据迁移响应模型"""
success: bool
message: str
migrated_counts: Dict[str, int]
errors: List[str] = []
# 登录相关模型
class LoginRequest(BaseModel):
"""登录请求模型"""
phone: str = Field(..., description="手机号")
password: str = Field(..., description="密码")
class LoginResponse(BaseModel):
"""登录响应模型"""
success: bool
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int # token过期时间(秒)
user: UserResponse
message: str = "登录成功"
class TokenRefreshRequest(BaseModel):
"""Token刷新请求模型"""
refresh_token: str = Field(..., description="刷新token")
class TokenRefreshResponse(BaseModel):
"""Token刷新响应模型"""
success: bool
access_token: str
token_type: str = "bearer"
expires_in: int # token过期时间(秒)
message: str = "Token刷新成功"
# FastAPI 核心依赖
fastapi>=0.104.1
uvicorn[standard]>=0.24.0
# 数据库相关
asyncpg>=0.29.0
databases[postgresql]>=0.8.0
sqlalchemy>=2.0.23
psycopg2-binary>=2.9.0
# 数据验证和序列化
pydantic>=2.5.0
pydantic-settings>=2.1.0
# HTTP 客户端和工具
httpx>=0.25.2
python-multipart>=0.0.6
# 日期时间处理
python-dateutil>=2.8.2
# JSON 处理(使用标准库,避免 Rust 依赖)
# orjson==3.9.10 # 需要 Rust 编译器,在 Windows 上可能有问题
# 日志记录
loguru>=0.7.2
# JWT 认证和密码加密
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
# 开发和调试工具
python-dotenv>=1.0.0
# 路由模块初始化文件
"""
历史数据 API 路由
提供月度历史统计数据的 CRUD 操作接口
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Dict, Any
from loguru import logger
from datetime import datetime
from database import monthly_history_table, get_database, DatabaseManager
from models import MonthlyHistoryCreate, MonthlyHistoryResponse, BaseResponse, UserResponse
from dependencies import get_current_active_user, require_admin
router = APIRouter()
@router.get("/", response_model=List[MonthlyHistoryResponse], summary="获取所有历史记录")
async def get_all_history(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""获取所有月度历史统计记录"""
try:
query = monthly_history_table.select().order_by(monthly_history_table.c.month.desc())
histories = await db.fetch_all(query)
return [
MonthlyHistoryResponse(
id=history["id"],
month=history["month"],
save_time=history["save_time"],
total_users=history["total_users"],
total_institutions=history["total_institutions"],
total_images=history["total_images"],
user_stats=history["user_stats"],
created_at=history["created_at"]
)
for history in histories
]
except Exception as e:
logger.error(f"获取历史记录失败: {e}")
raise HTTPException(status_code=500, detail="获取历史记录失败")
@router.get("/{month}", response_model=MonthlyHistoryResponse, summary="获取指定月份历史记录")
async def get_history_by_month(
month: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据月份获取历史统计记录"""
try:
# 验证月份格式
if not month or len(month) != 7 or month[4] != '-':
raise HTTPException(status_code=400, detail="月份格式错误,应为 YYYY-MM")
query = monthly_history_table.select().where(monthly_history_table.c.month == month)
history = await db.fetch_one(query)
if not history:
raise HTTPException(status_code=404, detail="指定月份的历史记录不存在")
return MonthlyHistoryResponse(
id=history["id"],
month=history["month"],
save_time=history["save_time"],
total_users=history["total_users"],
total_institutions=history["total_institutions"],
total_images=history["total_images"],
user_stats=history["user_stats"],
created_at=history["created_at"]
)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取指定月份历史记录失败: {e}")
raise HTTPException(status_code=500, detail="获取历史记录失败")
@router.post("/", response_model=BaseResponse, summary="保存月度历史记录")
async def save_monthly_history(
history_data: MonthlyHistoryCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""保存月度历史统计记录"""
try:
# 检查该月份是否已有记录
existing_history = await db.fetch_one(
monthly_history_table.select().where(
monthly_history_table.c.month == history_data.month
)
)
if existing_history:
# 更新现有记录
query = monthly_history_table.update().where(
monthly_history_table.c.month == history_data.month
).values(
save_time=history_data.save_time,
total_users=history_data.total_users,
total_institutions=history_data.total_institutions,
total_images=history_data.total_images,
user_stats=history_data.user_stats
)
await db.execute(query)
message = f"{history_data.month} 月度记录更新成功"
else:
# 创建新记录
query = monthly_history_table.insert().values(
month=history_data.month,
save_time=history_data.save_time,
total_users=history_data.total_users,
total_institutions=history_data.total_institutions,
total_images=history_data.total_images,
user_stats=history_data.user_stats
)
await db.execute(query)
message = f"{history_data.month} 月度记录保存成功"
return BaseResponse(message=message)
except Exception as e:
logger.error(f"保存月度历史记录失败: {e}")
raise HTTPException(status_code=500, detail="保存历史记录失败")
@router.delete("/{month}", response_model=BaseResponse, summary="删除指定月份历史记录")
async def delete_history_by_month(month: str, db: DatabaseManager = Depends(get_database)):
"""删除指定月份的历史统计记录"""
try:
# 验证月份格式
if not month or len(month) != 7 or month[4] != '-':
raise HTTPException(status_code=400, detail="月份格式错误,应为 YYYY-MM")
# 检查记录是否存在
existing_history = await db.fetch_one(
monthly_history_table.select().where(monthly_history_table.c.month == month)
)
if not existing_history:
raise HTTPException(status_code=404, detail="指定月份的历史记录不存在")
# 删除记录
query = monthly_history_table.delete().where(monthly_history_table.c.month == month)
await db.execute(query)
return BaseResponse(message=f"{month} 月度记录删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除历史记录失败: {e}")
raise HTTPException(status_code=500, detail="删除历史记录失败")
@router.get("/stats/summary", response_model=Dict[str, Any], summary="获取历史统计摘要")
async def get_history_summary(db: DatabaseManager = Depends(get_database)):
"""获取历史统计数据摘要"""
try:
# 获取总记录数
total_query = "SELECT COUNT(*) as total FROM monthly_history"
total_result = await db.fetch_one(total_query)
total_records = total_result["total"] if total_result else 0
# 获取最新记录
latest_query = monthly_history_table.select().order_by(
monthly_history_table.c.month.desc()
).limit(1)
latest_record = await db.fetch_one(latest_query)
# 获取最早记录
earliest_query = monthly_history_table.select().order_by(
monthly_history_table.c.month.asc()
).limit(1)
earliest_record = await db.fetch_one(earliest_query)
return {
"total_records": total_records,
"latest_month": latest_record["month"] if latest_record else None,
"earliest_month": earliest_record["month"] if earliest_record else None,
"latest_stats": {
"total_users": latest_record["total_users"],
"total_institutions": latest_record["total_institutions"],
"total_images": latest_record["total_images"],
"save_time": latest_record["save_time"]
} if latest_record else None
}
except Exception as e:
logger.error(f"获取历史统计摘要失败: {e}")
raise HTTPException(status_code=500, detail="获取统计摘要失败")
@router.post("/cleanup", response_model=BaseResponse, summary="清理历史数据")
async def cleanup_old_history(
keep_months: int = 12,
db: DatabaseManager = Depends(get_database)
):
"""清理旧的历史数据,保留最近N个月的记录"""
try:
if keep_months < 1:
raise HTTPException(status_code=400, detail="保留月数必须大于0")
# 获取需要保留的记录
keep_query = monthly_history_table.select().order_by(
monthly_history_table.c.month.desc()
).limit(keep_months)
keep_records = await db.fetch_all(keep_query)
if not keep_records:
return BaseResponse(message="没有历史记录需要清理")
# 获取最早需要保留的月份
oldest_keep_month = keep_records[-1]["month"]
# 删除更早的记录
delete_query = monthly_history_table.delete().where(
monthly_history_table.c.month < oldest_keep_month
)
deleted_count = await db.execute(delete_query)
return BaseResponse(
message=f"清理完成,删除了 {deleted_count} 条旧记录,保留最近 {keep_months} 个月的数据"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"清理历史数据失败: {e}")
raise HTTPException(status_code=500, detail="清理历史数据失败")
"""
机构管理 API 路由
提供机构和图片的 CRUD 操作接口
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List
from loguru import logger
from datetime import datetime
from database import (
institutions_table, institution_images_table,
get_database, DatabaseManager
)
from models import (
InstitutionCreate, InstitutionUpdate, InstitutionResponse,
InstitutionImage, InstitutionImageCreate, BaseResponse, UserResponse,
InstitutionBatchCreate, InstitutionBatchDelete, InstitutionBatchResponse
)
from dependencies import get_current_active_user, require_admin
router = APIRouter()
async def get_institution_with_images(institution_id: str, db: DatabaseManager):
"""获取机构及其图片信息"""
# 获取机构基本信息
inst_query = institutions_table.select().where(institutions_table.c.id == institution_id)
institution = await db.fetch_one(inst_query)
if not institution:
return None
# 获取机构图片
images_query = institution_images_table.select().where(
institution_images_table.c.institution_id == institution_id
).order_by(institution_images_table.c.upload_time)
images = await db.fetch_all(images_query)
# 构建响应数据
institution_images = [
InstitutionImage(
id=img["id"],
url=img["url"],
uploadTime=img["upload_time"]
)
for img in images
]
return InstitutionResponse(
id=institution["id"],
name=institution["name"],
institution_id=institution["institution_id"],
owner_id=institution["owner_id"],
images=institution_images,
created_at=institution["created_at"],
updated_at=institution["updated_at"]
)
@router.get("/", response_model=List[InstitutionResponse], summary="获取所有机构")
async def get_all_institutions(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""获取所有机构列表(包含图片信息)"""
try:
# 获取所有机构
query = institutions_table.select().order_by(institutions_table.c.created_at)
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
inst_with_images = await get_institution_with_images(institution["id"], db)
if inst_with_images:
result.append(inst_with_images)
return result
except Exception as e:
logger.error(f"获取机构列表失败: {e}")
raise HTTPException(status_code=500, detail="获取机构列表失败")
@router.get("/{institution_id}", response_model=InstitutionResponse, summary="根据ID获取机构")
async def get_institution_by_id(
institution_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据机构ID获取机构信息"""
try:
institution = await get_institution_with_images(institution_id, db)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return institution
except HTTPException:
raise
except Exception as e:
logger.error(f"获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.get("/owner/{owner_id}", response_model=List[InstitutionResponse], summary="根据负责人ID获取机构")
async def get_institutions_by_owner(
owner_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据负责人ID获取机构列表"""
try:
query = institutions_table.select().where(
institutions_table.c.owner_id == owner_id
).order_by(institutions_table.c.created_at)
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
inst_with_images = await get_institution_with_images(institution["id"], db)
if inst_with_images:
result.append(inst_with_images)
return result
except Exception as e:
logger.error(f"根据负责人获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.post("/", response_model=InstitutionResponse, summary="创建机构")
async def create_institution(
institution_data: InstitutionCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""创建新机构"""
try:
# 检查机构ID是否已存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_data.id)
)
if existing_inst:
raise HTTPException(status_code=400, detail="机构ID已存在")
# 检查机构编号是否已存在(如果提供了编号)
if institution_data.institution_id:
existing_inst_id = await db.fetch_one(
institutions_table.select().where(
institutions_table.c.institution_id == institution_data.institution_id
)
)
if existing_inst_id:
raise HTTPException(status_code=400, detail="机构编号已存在")
# 插入新机构
query = institutions_table.insert().values(
id=institution_data.id,
name=institution_data.name,
institution_id=institution_data.institution_id,
owner_id=institution_data.owner_id
)
await db.execute(query)
# 返回创建的机构信息
return await get_institution_by_id(institution_data.id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"创建机构失败: {e}")
raise HTTPException(status_code=500, detail="创建机构失败")
@router.put("/{institution_id}", response_model=InstitutionResponse, summary="更新机构")
async def update_institution(
institution_id: str,
institution_data: InstitutionUpdate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""更新机构信息"""
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 构建更新数据
update_data = {}
if institution_data.name is not None:
update_data["name"] = institution_data.name
if institution_data.institution_id is not None:
# 检查机构编号是否被其他机构使用
inst_id_check = await db.fetch_one(
institutions_table.select().where(
(institutions_table.c.institution_id == institution_data.institution_id) &
(institutions_table.c.id != institution_id)
)
)
if inst_id_check:
raise HTTPException(status_code=400, detail="机构编号已被其他机构使用")
update_data["institution_id"] = institution_data.institution_id
if institution_data.owner_id is not None:
update_data["owner_id"] = institution_data.owner_id
if not update_data:
raise HTTPException(status_code=400, detail="没有提供更新数据")
# 执行更新
query = institutions_table.update().where(
institutions_table.c.id == institution_id
).values(**update_data)
await db.execute(query)
# 返回更新后的机构信息
return await get_institution_by_id(institution_id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"更新机构失败: {e}")
raise HTTPException(status_code=500, detail="更新机构失败")
@router.delete("/{institution_id}", response_model=BaseResponse, summary="删除机构")
async def delete_institution(
institution_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""删除机构(级联删除相关图片)"""
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 删除机构(外键约束会自动删除相关图片)
query = institutions_table.delete().where(institutions_table.c.id == institution_id)
await db.execute(query)
return BaseResponse(message="机构删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除机构失败: {e}")
raise HTTPException(status_code=500, detail="删除机构失败")
# 图片管理相关接口
@router.post("/{institution_id}/images", response_model=BaseResponse, summary="添加机构图片")
async def add_institution_image(
institution_id: str,
image_data: InstitutionImageCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""为机构添加图片"""
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 检查图片ID是否已存在
existing_image = await db.fetch_one(
institution_images_table.select().where(institution_images_table.c.id == image_data.id)
)
if existing_image:
raise HTTPException(status_code=400, detail="图片ID已存在")
# 插入图片记录
query = institution_images_table.insert().values(
id=image_data.id,
institution_id=institution_id,
url=image_data.url,
upload_time=image_data.upload_time
)
await db.execute(query)
return BaseResponse(message="图片添加成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"添加机构图片失败: {e}")
raise HTTPException(status_code=500, detail="添加图片失败")
@router.delete("/{institution_id}/images/{image_id}", response_model=BaseResponse, summary="删除机构图片")
async def delete_institution_image(
institution_id: str,
image_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""删除机构图片"""
try:
# 检查图片是否存在且属于指定机构
existing_image = await db.fetch_one(
institution_images_table.select().where(
(institution_images_table.c.id == image_id) &
(institution_images_table.c.institution_id == institution_id)
)
)
if not existing_image:
raise HTTPException(status_code=404, detail="图片不存在")
# 删除图片记录
query = institution_images_table.delete().where(
institution_images_table.c.id == image_id
)
await db.execute(query)
return BaseResponse(message="图片删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除机构图片失败: {e}")
raise HTTPException(status_code=500, detail="删除图片失败")
@router.get("/institution-id/{inst_id}", response_model=InstitutionResponse, summary="根据机构编号获取机构")
async def get_institution_by_institution_id(
inst_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据机构编号获取机构信息"""
try:
query = institutions_table.select().where(institutions_table.c.institution_id == inst_id)
institution = await db.fetch_one(query)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return await get_institution_with_images(institution["id"], db)
except HTTPException:
raise
except Exception as e:
logger.error(f"根据机构编号获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.post("/batch", response_model=InstitutionBatchResponse, summary="批量创建机构")
async def batch_create_institutions(
batch_data: InstitutionBatchCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""批量创建机构"""
success_count = 0
error_count = 0
errors = []
try:
for i, institution_data in enumerate(batch_data.institutions):
try:
# 检查机构ID是否已存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_data.id)
)
if existing_inst:
errors.append(f"第{i+1}个机构: 机构ID {institution_data.id} 已存在")
error_count += 1
continue
# 检查机构编号是否已存在(如果提供了编号)
if institution_data.institution_id:
existing_inst_id = await db.fetch_one(
institutions_table.select().where(
institutions_table.c.institution_id == institution_data.institution_id
)
)
if existing_inst_id:
errors.append(f"第{i+1}个机构: 机构编号 {institution_data.institution_id} 已存在")
error_count += 1
continue
# 插入新机构
query = institutions_table.insert().values(
id=institution_data.id,
name=institution_data.name,
institution_id=institution_data.institution_id,
owner_id=institution_data.owner_id
)
await db.execute(query)
success_count += 1
except Exception as e:
errors.append(f"第{i+1}个机构: {str(e)}")
error_count += 1
message = f"批量创建完成: 成功 {success_count} 个, 失败 {error_count} 个"
logger.info(message)
return InstitutionBatchResponse(
success_count=success_count,
error_count=error_count,
errors=errors,
message=message
)
except Exception as e:
logger.error(f"批量创建机构失败: {e}")
raise HTTPException(status_code=500, detail="批量创建机构失败")
@router.get("/batch/test", summary="测试批量删除路由")
async def test_batch_route():
"""测试批量删除路由是否可访问"""
return {"message": "批量删除路由正常工作"}
@router.delete("/batch-delete", response_model=InstitutionBatchResponse, summary="批量删除机构")
async def batch_delete_institutions(
batch_data: InstitutionBatchDelete,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""批量删除机构"""
success_count = 0
error_count = 0
errors = []
try:
logger.info(f"收到批量删除请求,机构ID列表: {batch_data.institution_ids}")
for institution_id in batch_data.institution_ids:
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
errors.append(f"机构 {institution_id} 不存在")
error_count += 1
continue
# 删除机构(外键约束会自动删除相关图片)
query = institutions_table.delete().where(institutions_table.c.id == institution_id)
await db.execute(query)
success_count += 1
except Exception as e:
errors.append(f"删除机构 {institution_id} 失败: {str(e)}")
error_count += 1
message = f"批量删除完成: 成功 {success_count} 个, 失败 {error_count} 个"
logger.info(message)
return InstitutionBatchResponse(
success_count=success_count,
error_count=error_count,
errors=errors,
message=message
)
except Exception as e:
logger.error(f"批量删除机构失败: {e}")
raise HTTPException(status_code=500, detail="批量删除机构失败")
"""
机构管理 API 路由
提供机构和图片的 CRUD 操作接口
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List
from loguru import logger
from datetime import datetime
from database import (
institutions_table, institution_images_table,
get_database, DatabaseManager
)
from models import (
InstitutionCreate, InstitutionUpdate, InstitutionResponse,
InstitutionImage, InstitutionImageCreate, BaseResponse, UserResponse,
InstitutionBatchCreate, InstitutionBatchDelete, InstitutionBatchResponse
)
from dependencies import get_current_active_user, require_admin
router = APIRouter()
async def get_institution_with_images(institution_id: str, db: DatabaseManager):
"""获取机构及其图片信息"""
# 获取机构基本信息
inst_query = institutions_table.select().where(institutions_table.c.id == institution_id)
institution = await db.fetch_one(inst_query)
if not institution:
return None
# 获取机构图片
images_query = institution_images_table.select().where(
institution_images_table.c.institution_id == institution_id
).order_by(institution_images_table.c.upload_time)
images = await db.fetch_all(images_query)
# 构建响应数据
institution_images = [
InstitutionImage(
id=img["id"],
url=img["url"],
uploadTime=img["upload_time"]
)
for img in images
]
return InstitutionResponse(
id=institution["id"],
name=institution["name"],
institution_id=institution["institution_id"],
owner_id=institution["owner_id"],
images=institution_images,
created_at=institution["created_at"],
updated_at=institution["updated_at"]
)
@router.get("/", response_model=List[InstitutionResponse], summary="获取所有机构")
async def get_all_institutions(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""获取所有机构列表(包含图片信息)"""
try:
# 获取所有机构
query = institutions_table.select().order_by(institutions_table.c.created_at)
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
inst_with_images = await get_institution_with_images(institution["id"], db)
if inst_with_images:
result.append(inst_with_images)
return result
except Exception as e:
logger.error(f"获取机构列表失败: {e}")
raise HTTPException(status_code=500, detail="获取机构列表失败")
@router.get("/{institution_id}", response_model=InstitutionResponse, summary="根据ID获取机构")
async def get_institution_by_id(
institution_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据机构ID获取机构信息"""
try:
institution = await get_institution_with_images(institution_id, db)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return institution
except HTTPException:
raise
except Exception as e:
logger.error(f"获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.get("/owner/{owner_id}", response_model=List[InstitutionResponse], summary="根据负责人ID获取机构")
async def get_institutions_by_owner(
owner_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据负责人ID获取机构列表"""
try:
query = institutions_table.select().where(
institutions_table.c.owner_id == owner_id
).order_by(institutions_table.c.created_at)
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
inst_with_images = await get_institution_with_images(institution["id"], db)
if inst_with_images:
result.append(inst_with_images)
return result
except Exception as e:
logger.error(f"根据负责人获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.post("/", response_model=InstitutionResponse, summary="创建机构")
async def create_institution(
institution_data: InstitutionCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""创建新机构"""
try:
# 检查机构ID是否已存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_data.id)
)
if existing_inst:
raise HTTPException(status_code=400, detail="机构ID已存在")
# 检查机构编号是否已存在(如果提供了编号)
if institution_data.institution_id:
existing_inst_id = await db.fetch_one(
institutions_table.select().where(
institutions_table.c.institution_id == institution_data.institution_id
)
)
if existing_inst_id:
raise HTTPException(status_code=400, detail="机构编号已存在")
# 插入新机构
query = institutions_table.insert().values(
id=institution_data.id,
name=institution_data.name,
institution_id=institution_data.institution_id,
owner_id=institution_data.owner_id
)
await db.execute(query)
# 返回创建的机构信息
return await get_institution_by_id(institution_data.id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"创建机构失败: {e}")
raise HTTPException(status_code=500, detail="创建机构失败")
@router.put("/{institution_id}", response_model=InstitutionResponse, summary="更新机构")
async def update_institution(
institution_id: str,
institution_data: InstitutionUpdate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""更新机构信息"""
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 构建更新数据
update_data = {}
if institution_data.name is not None:
update_data["name"] = institution_data.name
if institution_data.institution_id is not None:
# 检查机构编号是否被其他机构使用
inst_id_check = await db.fetch_one(
institutions_table.select().where(
(institutions_table.c.institution_id == institution_data.institution_id) &
(institutions_table.c.id != institution_id)
)
)
if inst_id_check:
raise HTTPException(status_code=400, detail="机构编号已被其他机构使用")
update_data["institution_id"] = institution_data.institution_id
if institution_data.owner_id is not None:
update_data["owner_id"] = institution_data.owner_id
if not update_data:
raise HTTPException(status_code=400, detail="没有提供更新数据")
# 执行更新
query = institutions_table.update().where(
institutions_table.c.id == institution_id
).values(**update_data)
await db.execute(query)
# 返回更新后的机构信息
return await get_institution_by_id(institution_id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"更新机构失败: {e}")
raise HTTPException(status_code=500, detail="更新机构失败")
@router.delete("/{institution_id}", response_model=BaseResponse, summary="删除机构")
async def delete_institution(
institution_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""删除机构(级联删除相关图片)"""
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 删除机构(外键约束会自动删除相关图片)
query = institutions_table.delete().where(institutions_table.c.id == institution_id)
await db.execute(query)
return BaseResponse(message="机构删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除机构失败: {e}")
raise HTTPException(status_code=500, detail="删除机构失败")
# 图片管理相关接口
@router.post("/{institution_id}/images", response_model=BaseResponse, summary="添加机构图片")
async def add_institution_image(
institution_id: str,
image_data: InstitutionImageCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""为机构添加图片"""
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
raise HTTPException(status_code=404, detail="机构不存在")
# 检查图片ID是否已存在
existing_image = await db.fetch_one(
institution_images_table.select().where(institution_images_table.c.id == image_data.id)
)
if existing_image:
raise HTTPException(status_code=400, detail="图片ID已存在")
# 插入图片记录
query = institution_images_table.insert().values(
id=image_data.id,
institution_id=institution_id,
url=image_data.url,
upload_time=image_data.upload_time
)
await db.execute(query)
return BaseResponse(message="图片添加成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"添加机构图片失败: {e}")
raise HTTPException(status_code=500, detail="添加图片失败")
@router.delete("/{institution_id}/images/{image_id}", response_model=BaseResponse, summary="删除机构图片")
async def delete_institution_image(
institution_id: str,
image_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""删除机构图片"""
try:
# 检查图片是否存在且属于指定机构
existing_image = await db.fetch_one(
institution_images_table.select().where(
(institution_images_table.c.id == image_id) &
(institution_images_table.c.institution_id == institution_id)
)
)
if not existing_image:
raise HTTPException(status_code=404, detail="图片不存在")
# 删除图片记录
query = institution_images_table.delete().where(
institution_images_table.c.id == image_id
)
await db.execute(query)
return BaseResponse(message="图片删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除机构图片失败: {e}")
raise HTTPException(status_code=500, detail="删除图片失败")
@router.get("/institution-id/{inst_id}", response_model=InstitutionResponse, summary="根据机构编号获取机构")
async def get_institution_by_institution_id(
inst_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据机构编号获取机构信息"""
try:
query = institutions_table.select().where(institutions_table.c.institution_id == inst_id)
institution = await db.fetch_one(query)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return await get_institution_with_images(institution["id"], db)
except HTTPException:
raise
except Exception as e:
logger.error(f"根据机构编号获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.post("/batch", response_model=InstitutionBatchResponse, summary="批量创建机构")
async def batch_create_institutions(
batch_data: InstitutionBatchCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""批量创建机构"""
success_count = 0
error_count = 0
errors = []
try:
for i, institution_data in enumerate(batch_data.institutions):
try:
# 检查机构ID是否已存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_data.id)
)
if existing_inst:
errors.append(f"第{i+1}个机构: 机构ID {institution_data.id} 已存在")
error_count += 1
continue
# 检查机构编号是否已存在(如果提供了编号)
if institution_data.institution_id:
existing_inst_id = await db.fetch_one(
institutions_table.select().where(
institutions_table.c.institution_id == institution_data.institution_id
)
)
if existing_inst_id:
errors.append(f"第{i+1}个机构: 机构编号 {institution_data.institution_id} 已存在")
error_count += 1
continue
# 插入新机构
query = institutions_table.insert().values(
id=institution_data.id,
name=institution_data.name,
institution_id=institution_data.institution_id,
owner_id=institution_data.owner_id
)
await db.execute(query)
success_count += 1
except Exception as e:
errors.append(f"第{i+1}个机构: {str(e)}")
error_count += 1
message = f"批量创建完成: 成功 {success_count} 个, 失败 {error_count} 个"
logger.info(message)
return InstitutionBatchResponse(
success_count=success_count,
error_count=error_count,
errors=errors,
message=message
)
except Exception as e:
logger.error(f"批量创建机构失败: {e}")
raise HTTPException(status_code=500, detail="批量创建机构失败")
@router.get("/batch/test", summary="测试批量删除路由")
async def test_batch_route():
"""测试批量删除路由是否可访问"""
return {"message": "批量删除路由正常工作"}
@router.delete("/batch", response_model=InstitutionBatchResponse, summary="批量删除机构")
async def batch_delete_institutions(
batch_data: InstitutionBatchDelete,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""批量删除机构"""
success_count = 0
error_count = 0
errors = []
try:
logger.info(f"收到批量删除请求,机构ID列表: {batch_data.institution_ids}")
for institution_id in batch_data.institution_ids:
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
errors.append(f"机构 {institution_id} 不存在")
error_count += 1
continue
# 删除机构(外键约束会自动删除相关图片)
query = institutions_table.delete().where(institutions_table.c.id == institution_id)
await db.execute(query)
success_count += 1
except Exception as e:
errors.append(f"删除机构 {institution_id} 失败: {str(e)}")
error_count += 1
message = f"批量删除完成: 成功 {success_count} 个, 失败 {error_count} 个"
logger.info(message)
return InstitutionBatchResponse(
success_count=success_count,
error_count=error_count,
errors=errors,
message=message
)
except Exception as e:
logger.error(f"批量删除机构失败: {e}")
raise HTTPException(status_code=500, detail="批量删除机构失败")
"""机构管理路由 - 重新组织路由顺序"""
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from loguru import logger
from ..database import DatabaseManager, get_database
from ..database import institutions_table, institution_images_table
from ..models import (
InstitutionResponse, InstitutionCreate, InstitutionUpdate,
InstitutionBatchCreate, InstitutionBatchDelete, InstitutionBatchResponse,
InstitutionImageCreate, BaseResponse, UserResponse
)
from ..auth import get_current_active_user, require_admin
router = APIRouter()
async def get_institution_with_images(institution_id: str, db: DatabaseManager) -> InstitutionResponse:
"""获取机构信息(包含图片)"""
# 获取机构基本信息
institution_query = institutions_table.select().where(institutions_table.c.id == institution_id)
institution = await db.fetch_one(institution_query)
if not institution:
return None
# 获取机构图片
images_query = institution_images_table.select().where(
institution_images_table.c.institution_id == institution_id
)
images = await db.fetch_all(images_query)
# 转换图片格式
image_list = []
for img in images:
image_list.append({
"id": img["id"],
"url": img["url"],
"uploadTime": img["upload_time"].isoformat() if img["upload_time"] else None
})
return InstitutionResponse(
id=institution["id"],
institution_id=institution["institution_id"],
name=institution["name"],
owner_id=institution["owner_id"],
images=image_list,
created_at=institution["created_at"],
updated_at=institution["updated_at"]
)
def convert_institution_to_response(institution) -> InstitutionResponse:
"""将数据库记录转换为响应模型"""
return InstitutionResponse(
id=institution["id"],
institution_id=institution["institution_id"],
name=institution["name"],
owner_id=institution["owner_id"],
images=[], # 这里需要单独查询图片
created_at=institution["created_at"],
updated_at=institution["updated_at"]
)
# ============ 静态路由(必须在动态路由之前) ============
@router.get("/", response_model=List[InstitutionResponse], summary="获取所有机构")
async def get_all_institutions(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""获取所有机构列表(包含图片信息)"""
try:
query = institutions_table.select()
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
institution_with_images = await get_institution_with_images(institution["id"], db)
if institution_with_images:
result.append(institution_with_images)
return result
except Exception as e:
logger.error(f"获取机构列表失败: {e}")
raise HTTPException(status_code=500, detail="获取机构列表失败")
@router.get("/batch/test", summary="测试批量删除路由")
async def test_batch_route():
"""测试批量删除路由是否可访问"""
return {"message": "批量删除路由正常工作"}
@router.post("/batch", response_model=InstitutionBatchResponse, summary="批量创建机构")
async def batch_create_institutions(
batch_data: InstitutionBatchCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""批量创建机构"""
try:
success_count = 0
error_count = 0
errors = []
for i, institution_data in enumerate(batch_data.institutions):
try:
# 检查机构ID是否已存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_data.id)
)
if existing_inst:
errors.append(f"第{i+1}个机构: 机构ID {institution_data.id} 已存在")
error_count += 1
continue
# 检查机构编号是否已存在(如果提供了编号)
if institution_data.institution_id:
existing_inst_id = await db.fetch_one(
institutions_table.select().where(
institutions_table.c.institution_id == institution_data.institution_id
)
)
if existing_inst_id:
errors.append(f"第{i+1}个机构: 机构编号 {institution_data.institution_id} 已存在")
error_count += 1
continue
# 插入新机构
query = institutions_table.insert().values(
id=institution_data.id,
name=institution_data.name,
institution_id=institution_data.institution_id,
owner_id=institution_data.owner_id
)
await db.execute(query)
success_count += 1
except Exception as e:
errors.append(f"第{i+1}个机构: {str(e)}")
error_count += 1
message = f"批量创建完成: 成功 {success_count} 个,失败 {error_count} 个"
return InstitutionBatchResponse(
success_count=success_count,
error_count=error_count,
errors=errors,
message=message
)
except Exception as e:
logger.error(f"批量创建机构失败: {e}")
raise HTTPException(status_code=500, detail="批量创建机构失败")
@router.delete("/batch", response_model=InstitutionBatchResponse, summary="批量删除机构")
async def batch_delete_institutions(
batch_data: InstitutionBatchDelete,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""批量删除机构"""
success_count = 0
error_count = 0
errors = []
try:
logger.info(f"收到批量删除请求,机构ID列表: {batch_data.institution_ids}")
for institution_id in batch_data.institution_ids:
try:
# 检查机构是否存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == institution_id)
)
if not existing_inst:
errors.append(f"机构 {institution_id} 不存在")
error_count += 1
continue
# 删除机构的所有图片
await db.execute(
institution_images_table.delete().where(
institution_images_table.c.institution_id == institution_id
)
)
# 删除机构
await db.execute(
institutions_table.delete().where(institutions_table.c.id == institution_id)
)
success_count += 1
logger.info(f"成功删除机构: {institution_id}")
except Exception as e:
error_msg = f"删除机构 {institution_id} 失败: {str(e)}"
errors.append(error_msg)
error_count += 1
logger.error(error_msg)
message = f"批量删除完成: 成功 {success_count} 个,失败 {error_count} 个"
return InstitutionBatchResponse(
success_count=success_count,
error_count=error_count,
errors=errors,
message=message
)
except Exception as e:
logger.error(f"批量删除机构失败: {e}")
raise HTTPException(status_code=500, detail="批量删除机构失败")
@router.get("/owner/{owner_id}", response_model=List[InstitutionResponse], summary="根据负责人ID获取机构")
async def get_institutions_by_owner(
owner_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据负责人ID获取机构列表"""
try:
query = institutions_table.select().where(institutions_table.c.owner_id == owner_id)
institutions = await db.fetch_all(query)
result = []
for institution in institutions:
institution_with_images = await get_institution_with_images(institution["id"], db)
if institution_with_images:
result.append(institution_with_images)
return result
except Exception as e:
logger.error(f"根据负责人获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
@router.get("/institution-id/{inst_id}", response_model=InstitutionResponse, summary="根据机构编号获取机构")
async def get_institution_by_institution_id(
inst_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据机构编号获取机构信息"""
try:
query = institutions_table.select().where(institutions_table.c.institution_id == inst_id)
institution = await db.fetch_one(query)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return await get_institution_with_images(institution["id"], db)
except HTTPException:
raise
except Exception as e:
logger.error(f"根据机构编号获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
# ============ 动态路由(必须在静态路由之后) ============
@router.get("/{institution_id}", response_model=InstitutionResponse, summary="根据ID获取机构")
async def get_institution_by_id(
institution_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据机构ID获取机构信息"""
try:
institution = await get_institution_with_images(institution_id, db)
if not institution:
raise HTTPException(status_code=404, detail="机构不存在")
return institution
except HTTPException:
raise
except Exception as e:
logger.error(f"获取机构失败: {e}")
raise HTTPException(status_code=500, detail="获取机构失败")
"""
数据迁移 API 路由
提供 localStorage 数据迁移到数据库的功能
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Dict, Any
from loguru import logger
from datetime import datetime
import json
from database import (
users_table, institutions_table, institution_images_table,
system_config_table, monthly_history_table,
get_database, DatabaseManager
)
from models import MigrationData, MigrationResponse, BaseResponse
router = APIRouter()
async def migrate_users(users_data: List[Dict[str, Any]], db: DatabaseManager) -> Dict[str, int]:
"""迁移用户数据"""
migrated_count = 0
error_count = 0
errors = []
try:
for user in users_data:
try:
# 检查用户是否已存在
existing_user = await db.fetch_one(
users_table.select().where(users_table.c.id == user.get("id"))
)
if existing_user:
# 更新现有用户
query = users_table.update().where(users_table.c.id == user.get("id")).values(
phone=user.get("phone", ""),
password=user.get("password", ""),
name=user.get("name", ""),
role=user.get("role", "user"),
institutions=user.get("institutions", [])
)
await db.execute(query)
else:
# 创建新用户
query = users_table.insert().values(
id=user.get("id"),
phone=user.get("phone", ""),
password=user.get("password", ""),
name=user.get("name", ""),
role=user.get("role", "user"),
institutions=user.get("institutions", [])
)
await db.execute(query)
migrated_count += 1
except Exception as e:
error_count += 1
errors.append(f"用户 {user.get('id', 'unknown')} 迁移失败: {str(e)}")
logger.error(f"用户迁移失败: {e}")
except Exception as e:
logger.error(f"用户数据迁移过程失败: {e}")
raise
return {
"migrated": migrated_count,
"errors": error_count,
"error_messages": errors
}
async def migrate_institutions(institutions_data: List[Dict[str, Any]], db: DatabaseManager) -> Dict[str, int]:
"""迁移机构数据"""
migrated_count = 0
image_count = 0
error_count = 0
errors = []
try:
for institution in institutions_data:
try:
inst_id = institution.get("id")
# 检查机构是否已存在
existing_inst = await db.fetch_one(
institutions_table.select().where(institutions_table.c.id == inst_id)
)
if existing_inst:
# 更新现有机构
query = institutions_table.update().where(institutions_table.c.id == inst_id).values(
name=institution.get("name", ""),
institution_id=institution.get("institutionId"),
owner_id=institution.get("ownerId")
)
await db.execute(query)
else:
# 创建新机构
query = institutions_table.insert().values(
id=inst_id,
name=institution.get("name", ""),
institution_id=institution.get("institutionId"),
owner_id=institution.get("ownerId")
)
await db.execute(query)
# 处理机构图片
images = institution.get("images", [])
if images:
# 先删除现有图片
delete_images_query = institution_images_table.delete().where(
institution_images_table.c.institution_id == inst_id
)
await db.execute(delete_images_query)
# 插入新图片
for image in images:
try:
upload_time = image.get("uploadTime")
if isinstance(upload_time, str):
upload_time = datetime.fromisoformat(upload_time.replace('Z', '+00:00'))
elif not isinstance(upload_time, datetime):
upload_time = datetime.now()
image_query = institution_images_table.insert().values(
id=image.get("id"),
institution_id=inst_id,
url=image.get("url", ""),
upload_time=upload_time
)
await db.execute(image_query)
image_count += 1
except Exception as e:
errors.append(f"机构 {inst_id} 的图片 {image.get('id', 'unknown')} 迁移失败: {str(e)}")
migrated_count += 1
except Exception as e:
error_count += 1
errors.append(f"机构 {institution.get('id', 'unknown')} 迁移失败: {str(e)}")
logger.error(f"机构迁移失败: {e}")
except Exception as e:
logger.error(f"机构数据迁移过程失败: {e}")
raise
return {
"migrated": migrated_count,
"images": image_count,
"errors": error_count,
"error_messages": errors
}
async def migrate_system_config(config_data: Dict[str, Any], db: DatabaseManager) -> Dict[str, int]:
"""迁移系统配置数据"""
migrated_count = 0
error_count = 0
errors = []
try:
for key, value in config_data.items():
try:
# 检查配置是否已存在
existing_config = await db.fetch_one(
system_config_table.select().where(system_config_table.c.config_key == key)
)
if existing_config:
# 更新现有配置
query = system_config_table.update().where(
system_config_table.c.config_key == key
).values(config_value=value)
await db.execute(query)
else:
# 创建新配置
query = system_config_table.insert().values(
config_key=key,
config_value=value,
description=f"从localStorage迁移的配置项: {key}"
)
await db.execute(query)
migrated_count += 1
except Exception as e:
error_count += 1
errors.append(f"配置项 {key} 迁移失败: {str(e)}")
logger.error(f"配置迁移失败: {e}")
except Exception as e:
logger.error(f"系统配置迁移过程失败: {e}")
raise
return {
"migrated": migrated_count,
"errors": error_count,
"error_messages": errors
}
async def migrate_history_data(history_data: Dict[str, Any], db: DatabaseManager) -> Dict[str, int]:
"""迁移历史统计数据"""
migrated_count = 0
error_count = 0
errors = []
try:
if not history_data:
return {"migrated": 0, "errors": 0, "error_messages": []}
for month, data in history_data.items():
try:
# 解析保存时间
save_time = data.get("saveTime")
if isinstance(save_time, str):
save_time = datetime.fromisoformat(save_time.replace('Z', '+00:00'))
elif not isinstance(save_time, datetime):
save_time = datetime.now()
# 检查记录是否已存在
existing_history = await db.fetch_one(
monthly_history_table.select().where(monthly_history_table.c.month == month)
)
if existing_history:
# 更新现有记录
query = monthly_history_table.update().where(
monthly_history_table.c.month == month
).values(
save_time=save_time,
total_users=data.get("totalUsers", 0),
total_institutions=data.get("totalInstitutions", 0),
total_images=data.get("totalImages", 0),
user_stats=data.get("userStats", [])
)
await db.execute(query)
else:
# 创建新记录
query = monthly_history_table.insert().values(
month=month,
save_time=save_time,
total_users=data.get("totalUsers", 0),
total_institutions=data.get("totalInstitutions", 0),
total_images=data.get("totalImages", 0),
user_stats=data.get("userStats", [])
)
await db.execute(query)
migrated_count += 1
except Exception as e:
error_count += 1
errors.append(f"历史记录 {month} 迁移失败: {str(e)}")
logger.error(f"历史数据迁移失败: {e}")
except Exception as e:
logger.error(f"历史数据迁移过程失败: {e}")
raise
return {
"migrated": migrated_count,
"errors": error_count,
"error_messages": errors
}
@router.post("/migrate", response_model=MigrationResponse, summary="执行数据迁移")
async def migrate_data(
migration_data: MigrationData,
db: DatabaseManager = Depends(get_database)
):
"""执行完整的数据迁移过程"""
try:
logger.info("开始执行数据迁移...")
all_errors = []
migrated_counts = {
"users": 0,
"institutions": 0,
"images": 0,
"system_config": 0,
"history": 0
}
async with db.transaction():
# 迁移用户数据
if migration_data.users:
logger.info(f"开始迁移 {len(migration_data.users)} 个用户...")
user_result = await migrate_users(migration_data.users, db)
migrated_counts["users"] = user_result["migrated"]
all_errors.extend(user_result["error_messages"])
# 迁移机构数据
if migration_data.institutions:
logger.info(f"开始迁移 {len(migration_data.institutions)} 个机构...")
inst_result = await migrate_institutions(migration_data.institutions, db)
migrated_counts["institutions"] = inst_result["migrated"]
migrated_counts["images"] = inst_result["images"]
all_errors.extend(inst_result["error_messages"])
# 迁移系统配置
if migration_data.systemConfig:
logger.info(f"开始迁移 {len(migration_data.systemConfig)} 个配置项...")
config_result = await migrate_system_config(migration_data.systemConfig, db)
migrated_counts["system_config"] = config_result["migrated"]
all_errors.extend(config_result["error_messages"])
# 迁移历史数据
if migration_data.historyData:
logger.info(f"开始迁移 {len(migration_data.historyData)} 个历史记录...")
history_result = await migrate_history_data(migration_data.historyData, db)
migrated_counts["history"] = history_result["migrated"]
all_errors.extend(history_result["error_messages"])
logger.info("数据迁移完成")
return MigrationResponse(
success=True,
message="数据迁移完成",
migrated_counts=migrated_counts,
errors=all_errors
)
except Exception as e:
logger.error(f"数据迁移失败: {e}")
return MigrationResponse(
success=False,
message=f"数据迁移失败: {str(e)}",
migrated_counts={},
errors=[str(e)]
)
@router.post("/check", response_model=Dict[str, Any], summary="检查迁移状态")
async def check_migration_status(db: DatabaseManager = Depends(get_database)):
"""检查数据库中的数据状态,用于判断是否需要迁移"""
try:
# 检查各表的记录数
users_count = await db.fetch_one("SELECT COUNT(*) as count FROM users")
institutions_count = await db.fetch_one("SELECT COUNT(*) as count FROM institutions")
images_count = await db.fetch_one("SELECT COUNT(*) as count FROM institution_images")
config_count = await db.fetch_one("SELECT COUNT(*) as count FROM system_config")
history_count = await db.fetch_one("SELECT COUNT(*) as count FROM monthly_history")
return {
"database_status": "connected",
"data_counts": {
"users": users_count["count"] if users_count else 0,
"institutions": institutions_count["count"] if institutions_count else 0,
"images": images_count["count"] if images_count else 0,
"system_config": config_count["count"] if config_count else 0,
"history": history_count["count"] if history_count else 0
},
"migration_needed": (
(users_count["count"] if users_count else 0) <= 1 and # 只有默认管理员
(institutions_count["count"] if institutions_count else 0) == 0
),
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"检查迁移状态失败: {e}")
raise HTTPException(status_code=500, detail="检查迁移状态失败")
@router.post("/clear", response_model=BaseResponse, summary="清空数据库数据")
async def clear_database(
confirm: bool = False,
db: DatabaseManager = Depends(get_database)
):
"""清空数据库中的所有数据(危险操作,需要确认)"""
if not confirm:
raise HTTPException(status_code=400, detail="请设置 confirm=true 确认清空操作")
try:
async with db.transaction():
# 按依赖关系顺序删除数据
await db.execute(institution_images_table.delete())
await db.execute(institutions_table.delete())
await db.execute(monthly_history_table.delete())
await db.execute(system_config_table.delete())
await db.execute(users_table.delete())
# 重新插入默认数据
await db.execute(
users_table.insert().values(
id="admin",
phone="admin",
password="admin123",
name="系统管理员",
role="admin",
institutions=[]
)
)
# 插入默认系统配置
default_configs = [
("initialized", True, "系统是否已初始化"),
("version", "8.8.0", "系统版本"),
("hasDefaultData", False, "是否有默认示例数据")
]
for key, value, desc in default_configs:
await db.execute(
system_config_table.insert().values(
config_key=key,
config_value=value,
description=desc
)
)
logger.info("数据库已清空并重置为默认状态")
return BaseResponse(message="数据库已清空并重置为默认状态")
except Exception as e:
logger.error(f"清空数据库失败: {e}")
raise HTTPException(status_code=500, detail="清空数据库失败")
"""
系统配置 API 路由
提供系统配置的 CRUD 操作接口
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Dict, Any
from loguru import logger
from database import system_config_table, get_database, DatabaseManager
from models import SystemConfigItem, SystemConfigResponse, BaseResponse, UserResponse
from dependencies import get_current_active_user, require_admin
router = APIRouter()
@router.get("/", response_model=Dict[str, Any], summary="获取所有系统配置")
async def get_all_config(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""获取所有系统配置,返回键值对格式"""
try:
query = system_config_table.select()
configs = await db.fetch_all(query)
# 转换为键值对格式,与前端localStorage格式保持一致
result = {}
for config in configs:
result[config["config_key"]] = config["config_value"]
return result
except Exception as e:
logger.error(f"获取系统配置失败: {e}")
raise HTTPException(status_code=500, detail="获取系统配置失败")
@router.get("/list", response_model=List[SystemConfigResponse], summary="获取系统配置列表")
async def get_config_list(db: DatabaseManager = Depends(get_database)):
"""获取系统配置列表(详细格式)"""
try:
query = system_config_table.select().order_by(system_config_table.c.config_key)
configs = await db.fetch_all(query)
return [
SystemConfigResponse(
id=config["id"],
config_key=config["config_key"],
config_value=config["config_value"],
description=config["description"],
created_at=config["created_at"],
updated_at=config["updated_at"]
)
for config in configs
]
except Exception as e:
logger.error(f"获取系统配置列表失败: {e}")
raise HTTPException(status_code=500, detail="获取系统配置列表失败")
@router.get("/{config_key}", response_model=Any, summary="获取指定配置项")
async def get_config_by_key(config_key: str, db: DatabaseManager = Depends(get_database)):
"""根据配置键名获取配置值"""
try:
query = system_config_table.select().where(system_config_table.c.config_key == config_key)
config = await db.fetch_one(query)
if not config:
raise HTTPException(status_code=404, detail="配置项不存在")
return config["config_value"]
except HTTPException:
raise
except Exception as e:
logger.error(f"获取配置项失败: {e}")
raise HTTPException(status_code=500, detail="获取配置项失败")
@router.post("/", response_model=BaseResponse, summary="创建或更新配置项")
async def set_config(
config_item: SystemConfigItem,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""创建或更新系统配置项"""
try:
# 检查配置项是否已存在
existing_config = await db.fetch_one(
system_config_table.select().where(
system_config_table.c.config_key == config_item.config_key
)
)
if existing_config:
# 更新现有配置
query = system_config_table.update().where(
system_config_table.c.config_key == config_item.config_key
).values(
config_value=config_item.config_value,
description=config_item.description
)
await db.execute(query)
message = "配置项更新成功"
else:
# 创建新配置
query = system_config_table.insert().values(
config_key=config_item.config_key,
config_value=config_item.config_value,
description=config_item.description
)
await db.execute(query)
message = "配置项创建成功"
return BaseResponse(message=message)
except Exception as e:
logger.error(f"设置配置项失败: {e}")
raise HTTPException(status_code=500, detail="设置配置项失败")
@router.put("/", response_model=BaseResponse, summary="批量更新配置")
async def update_multiple_configs(
configs: Dict[str, Any],
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""批量更新系统配置"""
try:
async with db.transaction():
for key, value in configs.items():
# 检查配置项是否存在
existing_config = await db.fetch_one(
system_config_table.select().where(
system_config_table.c.config_key == key
)
)
if existing_config:
# 更新现有配置
query = system_config_table.update().where(
system_config_table.c.config_key == key
).values(config_value=value)
await db.execute(query)
else:
# 创建新配置
query = system_config_table.insert().values(
config_key=key,
config_value=value,
description=f"系统配置项: {key}"
)
await db.execute(query)
return BaseResponse(message=f"成功更新 {len(configs)} 个配置项")
except Exception as e:
logger.error(f"批量更新配置失败: {e}")
raise HTTPException(status_code=500, detail="批量更新配置失败")
@router.delete("/{config_key}", response_model=BaseResponse, summary="删除配置项")
async def delete_config(
config_key: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""删除系统配置项"""
try:
# 检查配置项是否存在
existing_config = await db.fetch_one(
system_config_table.select().where(system_config_table.c.config_key == config_key)
)
if not existing_config:
raise HTTPException(status_code=404, detail="配置项不存在")
# 不允许删除关键系统配置
protected_keys = ["initialized", "version"]
if config_key in protected_keys:
raise HTTPException(status_code=400, detail="不能删除系统关键配置")
# 删除配置项
query = system_config_table.delete().where(system_config_table.c.config_key == config_key)
await db.execute(query)
return BaseResponse(message="配置项删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除配置项失败: {e}")
raise HTTPException(status_code=500, detail="删除配置项失败")
@router.post("/reset", response_model=BaseResponse, summary="重置为默认配置")
async def reset_to_default(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""重置系统配置为默认值"""
try:
# 默认配置项
default_configs = [
("initialized", True, "系统是否已初始化"),
("version", "8.8.0", "系统版本"),
("hasDefaultData", False, "是否有默认示例数据")
]
async with db.transaction():
# 清空现有配置
await db.execute(system_config_table.delete())
# 插入默认配置
for key, value, desc in default_configs:
query = system_config_table.insert().values(
config_key=key,
config_value=value,
description=desc
)
await db.execute(query)
return BaseResponse(message="系统配置已重置为默认值")
except Exception as e:
logger.error(f"重置配置失败: {e}")
raise HTTPException(status_code=500, detail="重置配置失败")
"""
用户管理 API 路由
提供用户的 CRUD 操作接口
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List
from loguru import logger
from database import users_table, get_database, DatabaseManager
from models import (
UserCreate, UserUpdate, UserResponse, BaseResponse,
LoginRequest, LoginResponse, TokenRefreshRequest, TokenRefreshResponse
)
from auth import jwt_manager, password_manager
from dependencies import (
get_current_user, get_current_active_user, require_admin,
token_blacklist, verify_token_not_blacklisted
)
from config import settings
router = APIRouter()
@router.get("/", response_model=List[UserResponse], summary="获取所有用户")
async def get_all_users(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""获取所有用户列表"""
try:
query = users_table.select()
users = await db.fetch_all(query)
return [
UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
role=user["role"],
institutions=user["institutions"] or [],
created_at=user["created_at"],
updated_at=user["updated_at"]
)
for user in users
]
except Exception as e:
logger.error(f"获取用户列表失败: {e}")
raise HTTPException(status_code=500, detail="获取用户列表失败")
@router.get("/me", response_model=UserResponse, summary="获取当前用户信息")
async def get_current_user_info(current_user: UserResponse = Depends(get_current_active_user)):
"""获取当前登录用户的信息"""
return current_user
@router.get("/{user_id}", response_model=UserResponse, summary="根据ID获取用户")
async def get_user_by_id(
user_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(get_current_active_user)
):
"""根据用户ID获取用户信息"""
try:
query = users_table.select().where(users_table.c.id == user_id)
user = await db.fetch_one(query)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
role=user["role"],
institutions=user["institutions"] or [],
created_at=user["created_at"],
updated_at=user["updated_at"]
)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取用户失败: {e}")
raise HTTPException(status_code=500, detail="获取用户失败")
@router.post("/", response_model=UserResponse, summary="创建用户")
async def create_user(
user_data: UserCreate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""创建新用户"""
try:
# 检查用户ID是否已存在
existing_user = await db.fetch_one(
users_table.select().where(users_table.c.id == user_data.id)
)
if existing_user:
raise HTTPException(status_code=400, detail="用户ID已存在")
# 检查手机号是否已存在
existing_phone = await db.fetch_one(
users_table.select().where(users_table.c.phone == user_data.phone)
)
if existing_phone:
raise HTTPException(status_code=400, detail="手机号已存在")
# 加密密码
hashed_password = password_manager.hash_password(user_data.password)
# 插入新用户
query = users_table.insert().values(
id=user_data.id,
phone=user_data.phone,
password=hashed_password,
name=user_data.name,
role=user_data.role,
institutions=user_data.institutions
)
await db.execute(query)
# 返回创建的用户信息
return await get_user_by_id(user_data.id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"创建用户失败: {e}")
raise HTTPException(status_code=500, detail="创建用户失败")
@router.put("/{user_id}", response_model=UserResponse, summary="更新用户")
async def update_user(
user_id: str,
user_data: UserUpdate,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""更新用户信息"""
try:
# 检查用户是否存在
existing_user = await db.fetch_one(
users_table.select().where(users_table.c.id == user_id)
)
if not existing_user:
raise HTTPException(status_code=404, detail="用户不存在")
# 构建更新数据
update_data = {}
if user_data.phone is not None:
# 检查手机号是否被其他用户使用
phone_check = await db.fetch_one(
users_table.select().where(
(users_table.c.phone == user_data.phone) &
(users_table.c.id != user_id)
)
)
if phone_check:
raise HTTPException(status_code=400, detail="手机号已被其他用户使用")
update_data["phone"] = user_data.phone
if user_data.name is not None:
update_data["name"] = user_data.name
if user_data.password is not None:
# 加密新密码
update_data["password"] = password_manager.hash_password(user_data.password)
if user_data.role is not None:
update_data["role"] = user_data.role
if user_data.institutions is not None:
update_data["institutions"] = user_data.institutions
if not update_data:
raise HTTPException(status_code=400, detail="没有提供更新数据")
# 执行更新
query = users_table.update().where(users_table.c.id == user_id).values(**update_data)
await db.execute(query)
# 返回更新后的用户信息
return await get_user_by_id(user_id, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"更新用户失败: {e}")
raise HTTPException(status_code=500, detail="更新用户失败")
@router.delete("/{user_id}", response_model=BaseResponse, summary="删除用户")
async def delete_user(
user_id: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""删除用户"""
try:
# 检查用户是否存在
existing_user = await db.fetch_one(
users_table.select().where(users_table.c.id == user_id)
)
if not existing_user:
raise HTTPException(status_code=404, detail="用户不存在")
# 不允许删除管理员用户
if existing_user["role"] == "admin":
raise HTTPException(status_code=400, detail="不能删除管理员用户")
# 删除用户(外键约束会自动处理相关数据)
query = users_table.delete().where(users_table.c.id == user_id)
await db.execute(query)
return BaseResponse(message="用户删除成功")
except HTTPException:
raise
except Exception as e:
logger.error(f"删除用户失败: {e}")
raise HTTPException(status_code=500, detail="删除用户失败")
@router.post("/login", response_model=LoginResponse, summary="用户登录")
async def login(login_data: LoginRequest, db: DatabaseManager = Depends(get_database)):
"""用户登录验证"""
try:
# 查找用户
query = users_table.select().where(users_table.c.phone == login_data.phone)
user = await db.fetch_one(query)
if not user:
logger.warning(f"登录失败:用户不存在 - {login_data.phone}")
raise HTTPException(status_code=401, detail="手机号或密码错误")
# 验证密码(支持明文和加密密码)
password_valid = False
if password_manager.is_hashed_password(user["password"]):
# 已加密的密码,使用bcrypt验证
password_valid = password_manager.verify_password(login_data.password, user["password"])
else:
# 明文密码,直接比较(兼容旧数据)
password_valid = user["password"] == login_data.password
# 如果明文密码验证成功,自动升级为加密密码
if password_valid:
hashed_password = password_manager.hash_password(login_data.password)
update_query = users_table.update().where(
users_table.c.id == user["id"]
).values(password=hashed_password)
await db.execute(update_query)
logger.info(f"用户 {user['name']} 的密码已自动升级为加密存储")
if not password_valid:
logger.warning(f"登录失败:密码错误 - {login_data.phone}")
raise HTTPException(status_code=401, detail="手机号或密码错误")
# 创建用户信息
user_response = UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
role=user["role"],
institutions=user["institutions"] or [],
created_at=user["created_at"],
updated_at=user["updated_at"]
)
# 创建JWT token数据
token_data = {
"sub": user["phone"], # subject (用户标识)
"user_id": user["id"],
"role": user["role"],
"phone": user["phone"]
}
# 生成tokens
access_token = jwt_manager.create_access_token(token_data)
refresh_token = jwt_manager.create_refresh_token(token_data)
logger.info(f"用户登录成功: {user['name']} ({user['phone']})")
return LoginResponse(
success=True,
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=settings.JWT_EXPIRE_HOURS * 3600, # 转换为秒
user=user_response,
message="登录成功"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"用户登录失败: {e}")
raise HTTPException(status_code=500, detail="登录失败")
@router.post("/refresh", response_model=TokenRefreshResponse, summary="刷新访问token")
async def refresh_token(refresh_data: TokenRefreshRequest):
"""使用刷新token获取新的访问token"""
try:
# 生成新的访问token
new_access_token = jwt_manager.refresh_access_token(refresh_data.refresh_token)
logger.info("Token刷新成功")
return TokenRefreshResponse(
success=True,
access_token=new_access_token,
token_type="bearer",
expires_in=settings.JWT_EXPIRE_HOURS * 3600,
message="Token刷新成功"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Token刷新失败: {e}")
raise HTTPException(status_code=500, detail="Token刷新失败")
@router.post("/logout", response_model=BaseResponse, summary="用户登出")
async def logout(
current_user: UserResponse = Depends(get_current_active_user),
credentials = Depends(verify_token_not_blacklisted)
):
"""用户登出,将token加入黑名单"""
try:
# 将当前token加入黑名单
token_blacklist.add_token(credentials.credentials)
logger.info(f"用户登出成功: {current_user.name}")
return BaseResponse(
success=True,
message="登出成功"
)
except Exception as e:
logger.error(f"用户登出失败: {e}")
raise HTTPException(status_code=500, detail="登出失败")
@router.get("/phone/{phone}", response_model=UserResponse, summary="根据手机号获取用户")
async def get_user_by_phone(
phone: str,
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""根据手机号获取用户信息"""
try:
query = users_table.select().where(users_table.c.phone == phone)
user = await db.fetch_one(query)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return UserResponse(
id=user["id"],
phone=user["phone"],
name=user["name"],
role=user["role"],
institutions=user["institutions"] or [],
created_at=user["created_at"],
updated_at=user["updated_at"]
)
except HTTPException:
raise
except Exception as e:
logger.error(f"根据手机号获取用户失败: {e}")
raise HTTPException(status_code=500, detail="获取用户失败")
@router.post("/migrate-passwords", response_model=BaseResponse, summary="迁移明文密码为加密密码")
async def migrate_passwords(
db: DatabaseManager = Depends(get_database),
current_user: UserResponse = Depends(require_admin)
):
"""将数据库中的明文密码迁移为加密密码"""
try:
# 获取所有用户
query = users_table.select()
users = await db.fetch_all(query)
migrated_count = 0
for user in users:
# 检查密码是否已经是哈希格式
if not password_manager.is_hashed_password(user["password"]):
# 加密明文密码
hashed_password = password_manager.hash_password(user["password"])
# 更新数据库
update_query = users_table.update().where(
users_table.c.id == user["id"]
).values(password=hashed_password)
await db.execute(update_query)
migrated_count += 1
logger.info(f"用户 {user['name']} 的密码已迁移为加密存储")
return BaseResponse(
success=True,
message=f"密码迁移完成,共迁移 {migrated_count} 个用户的密码"
)
except Exception as e:
logger.error(f"密码迁移失败: {e}")
raise HTTPException(status_code=500, detail="密码迁移失败")
-- 绩效计分系统数据库初始化脚本
-- 基于localStorage数据结构设计的PostgreSQL表结构
-- 删除已存在的表(开发环境)
DROP TABLE IF EXISTS monthly_history CASCADE;
DROP TABLE IF EXISTS institution_images CASCADE;
DROP TABLE IF EXISTS institutions CASCADE;
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS system_config CASCADE;
-- 创建用户表 (对应 localStorage: score_system_users)
CREATE TABLE users (
id VARCHAR(50) PRIMARY KEY, -- 用户ID,如 'admin', 'user_1234567890'
phone VARCHAR(20) UNIQUE NOT NULL, -- 手机号,用于登录
password VARCHAR(255) NOT NULL, -- 密码(明文存储,与原系统保持一致)
name VARCHAR(100) NOT NULL, -- 用户姓名
role VARCHAR(20) DEFAULT 'user', -- 角色:'admin' 或 'user'
institutions JSONB DEFAULT '[]', -- 负责的机构ID数组,保持原有结构
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建机构表 (对应 localStorage: score_system_institutions)
CREATE TABLE institutions (
id VARCHAR(50) PRIMARY KEY, -- 内部ID,如 'inst_1234567890_abc123'
institution_id VARCHAR(50), -- 机构编号,如 '001', '002'
name VARCHAR(200) NOT NULL, -- 机构名称
owner_id VARCHAR(50), -- 负责人ID,可为NULL(公池机构)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 外键约束
CONSTRAINT fk_institutions_owner FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL,
-- 唯一约束
CONSTRAINT uk_institutions_institution_id UNIQUE (institution_id)
);
-- 创建机构图片表 (对应 institutions.images 数组)
CREATE TABLE institution_images (
id VARCHAR(50) PRIMARY KEY, -- 图片ID
institution_id VARCHAR(50) NOT NULL, -- 所属机构ID
url TEXT NOT NULL, -- 图片URL/Base64数据
upload_time TIMESTAMP NOT NULL, -- 上传时间
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 外键约束
CONSTRAINT fk_images_institution FOREIGN KEY (institution_id) REFERENCES institutions(id) ON DELETE CASCADE
);
-- 创建系统配置表 (对应 localStorage: score_system_config)
CREATE TABLE system_config (
id SERIAL PRIMARY KEY,
config_key VARCHAR(100) UNIQUE NOT NULL, -- 配置键名
config_value JSONB, -- 配置值(JSON格式)
description TEXT, -- 配置描述
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建月度历史统计表 (对应 localStorage: score_system_history)
CREATE TABLE monthly_history (
id SERIAL PRIMARY KEY,
month VARCHAR(7) NOT NULL, -- 月份,格式:YYYY-MM
save_time TIMESTAMP NOT NULL, -- 保存时间
total_users INTEGER NOT NULL, -- 总用户数
total_institutions INTEGER NOT NULL, -- 总机构数
total_images INTEGER NOT NULL, -- 总图片数
user_stats JSONB NOT NULL, -- 用户统计数据(JSON格式)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 唯一约束:每月只能有一条记录
CONSTRAINT uk_monthly_history_month UNIQUE (month)
);
-- 创建索引以提高查询性能
CREATE INDEX idx_users_phone ON users(phone);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_institutions_owner_id ON institutions(owner_id);
CREATE INDEX idx_institutions_institution_id ON institutions(institution_id);
CREATE INDEX idx_institution_images_institution_id ON institution_images(institution_id);
CREATE INDEX idx_institution_images_upload_time ON institution_images(upload_time);
CREATE INDEX idx_system_config_key ON system_config(config_key);
CREATE INDEX idx_monthly_history_month ON monthly_history(month);
-- 创建更新时间触发器函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 为相关表创建更新时间触发器
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_institutions_updated_at
BEFORE UPDATE ON institutions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_system_config_updated_at
BEFORE UPDATE ON system_config
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 插入默认系统配置
INSERT INTO system_config (config_key, config_value, description) VALUES
('initialized', 'true', '系统是否已初始化'),
('version', '"8.8.0"', '系统版本'),
('hasDefaultData', 'false', '是否有默认示例数据')
ON CONFLICT (config_key) DO UPDATE SET
config_value = EXCLUDED.config_value,
updated_at = CURRENT_TIMESTAMP;
-- 插入默认管理员用户
INSERT INTO users (id, phone, password, name, role, institutions) VALUES
('admin', 'admin', 'admin123', '系统管理员', 'admin', '[]')
ON CONFLICT (id) DO UPDATE SET
phone = EXCLUDED.phone,
password = EXCLUDED.password,
name = EXCLUDED.name,
role = EXCLUDED.role,
institutions = EXCLUDED.institutions,
updated_at = CURRENT_TIMESTAMP;
-- 创建数据迁移辅助视图
CREATE OR REPLACE VIEW v_institutions_with_images AS
SELECT
i.id,
i.institution_id,
i.name,
i.owner_id,
i.created_at,
i.updated_at,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'id', img.id,
'url', img.url,
'uploadTime', img.upload_time
) ORDER BY img.upload_time
) FILTER (WHERE img.id IS NOT NULL),
'[]'::json
) AS images
FROM institutions i
LEFT JOIN institution_images img ON i.id = img.institution_id
GROUP BY i.id, i.institution_id, i.name, i.owner_id, i.created_at, i.updated_at;
-- 创建数据完整性检查函数
CREATE OR REPLACE FUNCTION check_data_integrity()
RETURNS TABLE(
check_name TEXT,
status TEXT,
message TEXT,
count INTEGER
) AS $$
BEGIN
-- 检查孤立机构(负责人不存在)
RETURN QUERY
SELECT
'orphan_institutions'::TEXT,
CASE WHEN COUNT(*) = 0 THEN 'OK' ELSE 'WARNING' END::TEXT,
('发现 ' || COUNT(*) || ' 个孤立机构')::TEXT,
COUNT(*)::INTEGER
FROM institutions i
LEFT JOIN users u ON i.owner_id = u.id
WHERE i.owner_id IS NOT NULL AND u.id IS NULL;
-- 检查重复的机构编号
RETURN QUERY
SELECT
'duplicate_institution_ids'::TEXT,
CASE WHEN COUNT(*) = 0 THEN 'OK' ELSE 'ERROR' END::TEXT,
('发现 ' || COUNT(*) || ' 个重复的机构编号')::TEXT,
COUNT(*)::INTEGER
FROM (
SELECT institution_id, COUNT(*) as cnt
FROM institutions
WHERE institution_id IS NOT NULL
GROUP BY institution_id
HAVING COUNT(*) > 1
) duplicates;
-- 检查无效图片记录
RETURN QUERY
SELECT
'invalid_images'::TEXT,
CASE WHEN COUNT(*) = 0 THEN 'OK' ELSE 'WARNING' END::TEXT,
('发现 ' || COUNT(*) || ' 个无效图片记录')::TEXT,
COUNT(*)::INTEGER
FROM institution_images img
LEFT JOIN institutions i ON img.institution_id = i.id
WHERE i.id IS NULL;
END;
$$ LANGUAGE plpgsql;
COMMENT ON TABLE users IS '用户表,存储系统用户信息';
COMMENT ON TABLE institutions IS '机构表,存储机构基本信息';
COMMENT ON TABLE institution_images IS '机构图片表,存储机构上传的图片信息';
COMMENT ON TABLE system_config IS '系统配置表,存储系统配置参数';
COMMENT ON TABLE monthly_history IS '月度历史统计表,存储每月的统计数据';
COMMENT ON VIEW v_institutions_with_images IS '机构及其图片的聚合视图,便于数据查询';
COMMENT ON FUNCTION check_data_integrity() IS '数据完整性检查函数,用于验证数据一致性';
version: '3.8'
services: services:
scoring-app: # PostgreSQL 数据库服务
postgres:
image: postgres:15-alpine
container_name: performance_postgres
restart: unless-stopped
environment:
POSTGRES_DB: performance_db
POSTGRES_USER: performance_user
POSTGRES_PASSWORD: performance_pass
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./database/init_performance_system.sql:/docker-entrypoint-initdb.d/01-init.sql
networks:
- performance_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U performance_user -d performance_db"]
interval: 10s
timeout: 5s
retries: 5
# FastAPI 后端服务
api:
build:
context: ./backend
dockerfile: Dockerfile
container_name: performance_api
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://performance_user:performance_pass@postgres:5432/performance_db
- CORS_ORIGINS=http://localhost:5173,http://localhost:8080,http://localhost:4001
- API_HOST=0.0.0.0
- API_PORT=8000
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
volumes:
- ./backend:/app
- api_logs:/app/logs
networks:
- performance_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
# 前端应用服务
frontend:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: scoring-app-8.8 args:
- VITE_API_BASE_URL=http://localhost:8000
container_name: performance_frontend
ports: ports:
- "4001:80" - "4001:80"
restart: unless-stopped restart: unless-stopped
environment: environment:
- NODE_ENV=production - NODE_ENV=production
# 如果后续需要挂载静态资源或配置: depends_on:
# volumes: - api
# - ./some-dir:/usr/share/nginx/html/some-dir:ro networks:
- performance_network
# 数据卷定义
volumes:
postgres_data:
driver: local
name: performance_postgres_data
api_logs:
driver: local
name: performance_api_logs
# 网络定义
networks:
performance_network:
driver: bridge
name: performance_network
...@@ -24,12 +24,23 @@ app.use(pinia) ...@@ -24,12 +24,23 @@ app.use(pinia)
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
// 初始化数据和认证状态 // 初始化应用
const dataStore = useDataStore() const initializeApp = async () => {
const authStore = useAuthStore() try {
console.log('🚀 正在初始化绩效计分系统...')
// 先加载数据,再恢复认证状态 // 挂载应用(认证状态恢复在路由守卫中处理)
dataStore.loadFromStorage() app.mount('#app')
authStore.restoreAuth()
app.mount('#app') console.log('🎉 绩效计分系统初始化完成')
\ No newline at end of file
} catch (error) {
console.error('❌ 应用初始化失败:', error)
// 即使初始化失败,也要挂载应用(降级模式)
app.mount('#app')
}
}
// 启动应用
initializeApp()
\ No newline at end of file
...@@ -49,14 +49,23 @@ const router = createRouter({ ...@@ -49,14 +49,23 @@ const router = createRouter({
/** /**
* 路由守卫 - 检查用户认证状态和权限 * 路由守卫 - 检查用户认证状态和权限
*/ */
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore() const authStore = useAuthStore()
// 设置页面标题 // 设置页面标题
if (to.meta.title) { if (to.meta.title) {
document.title = `${to.meta.title} - 绩效计分系统` document.title = `${to.meta.title} - 绩效计分系统`
} }
// 如果是首次访问或刷新页面,且有token,等待认证状态恢复
if (!authStore.isAuthenticated && localStorage.getItem('auth_tokens')) {
try {
await authStore.restoreAuth()
} catch (error) {
console.warn('认证状态恢复失败:', error)
}
}
// 检查是否需要认证 // 检查是否需要认证
if (to.meta.requiresAuth) { if (to.meta.requiresAuth) {
if (!authStore.isAuthenticated) { if (!authStore.isAuthenticated) {
...@@ -64,7 +73,7 @@ router.beforeEach((to, from, next) => { ...@@ -64,7 +73,7 @@ router.beforeEach((to, from, next) => {
next('/login') next('/login')
return return
} }
// 检查角色权限 // 检查角色权限
if (to.meta.roles && !to.meta.roles.includes(authStore.currentUser.role)) { if (to.meta.roles && !to.meta.roles.includes(authStore.currentUser.role)) {
// 权限不足,跳转到用户面板 // 权限不足,跳转到用户面板
...@@ -72,7 +81,7 @@ router.beforeEach((to, from, next) => { ...@@ -72,7 +81,7 @@ router.beforeEach((to, from, next) => {
return return
} }
} }
// 已登录用户访问登录页,直接跳转到对应面板 // 已登录用户访问登录页,直接跳转到对应面板
if (to.path === '/login' && authStore.isAuthenticated) { if (to.path === '/login' && authStore.isAuthenticated) {
if (authStore.currentUser.role === 'admin') { if (authStore.currentUser.role === 'admin') {
...@@ -82,7 +91,7 @@ router.beforeEach((to, from, next) => { ...@@ -82,7 +91,7 @@ router.beforeEach((to, from, next) => {
} }
return return
} }
next() next()
}) })
......
/**
* API 服务封装层
* 提供与后端 FastAPI 服务的通信接口
* 替换原有的 localStorage 存储机制
*/
// API 基础配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
/**
* HTTP 请求封装类
*/
class ApiClient {
constructor(baseURL = API_BASE_URL) {
this.baseURL = baseURL
this.timeout = 10000 // 10秒超时
}
/**
* 获取认证头
*/
getAuthHeaders() {
const token = this.getAccessToken()
if (token) {
return {
'Authorization': `Bearer ${token}`
}
}
return {}
}
/**
* 获取访问token
*/
getAccessToken() {
try {
const authData = localStorage.getItem('auth_tokens')
if (authData) {
const tokens = JSON.parse(authData)
return tokens.access_token
}
} catch (error) {
console.error('获取访问token失败:', error)
}
return null
}
/**
* 获取刷新token
*/
getRefreshToken() {
try {
const authData = localStorage.getItem('auth_tokens')
if (authData) {
const tokens = JSON.parse(authData)
return tokens.refresh_token
}
} catch (error) {
console.error('获取刷新token失败:', error)
}
return null
}
/**
* 保存tokens
*/
saveTokens(accessToken, refreshToken) {
try {
const tokens = {
access_token: accessToken,
refresh_token: refreshToken,
timestamp: Date.now()
}
localStorage.setItem('auth_tokens', JSON.stringify(tokens))
} catch (error) {
console.error('保存tokens失败:', error)
}
}
/**
* 清除tokens
*/
clearTokens() {
localStorage.removeItem('auth_tokens')
}
/**
* 通用请求方法
*/
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`
const config = {
timeout: this.timeout,
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
...options.headers
},
...options
}
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
const response = await fetch(url, {
...config,
signal: controller.signal
})
clearTimeout(timeoutId)
// 处理401未授权响应
if (response.status === 401) {
// 尝试刷新token
const refreshed = await this.refreshAccessToken()
if (refreshed) {
// 重新发送原始请求
const retryConfig = {
...config,
headers: {
...config.headers,
...this.getAuthHeaders()
}
}
const retryResponse = await fetch(url, {
...retryConfig,
signal: controller.signal
})
if (retryResponse.ok) {
return await retryResponse.json()
}
}
// 刷新失败或重试失败,清除tokens并跳转到登录页
this.clearTokens()
this.handleAuthError()
throw new Error('认证失败,请重新登录')
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络连接')
}
console.error(`API请求失败 [${endpoint}]:`, error)
throw error
}
}
/**
* 刷新访问token
*/
async refreshAccessToken() {
try {
const refreshToken = this.getRefreshToken()
if (!refreshToken) {
return false
}
const response = await fetch(`${this.baseURL}/api/users/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refresh_token: refreshToken
})
})
if (response.ok) {
const data = await response.json()
if (data.success && data.access_token) {
// 更新访问token,保持刷新token不变
this.saveTokens(data.access_token, refreshToken)
console.log('✅ Token刷新成功')
return true
}
}
console.log('❌ Token刷新失败')
return false
} catch (error) {
console.error('Token刷新异常:', error)
return false
}
}
/**
* 处理认证错误
*/
handleAuthError() {
// 清除用户数据
localStorage.removeItem('currentUser')
// 跳转到登录页(如果不在登录页)
if (window.location.pathname !== '/login') {
window.location.href = '/login'
}
}
// GET 请求
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString()
const url = queryString ? `${endpoint}?${queryString}` : endpoint
return this.request(url, { method: 'GET' })
}
// POST 请求
async post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
})
}
// PUT 请求
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
})
}
// DELETE 请求
async delete(endpoint, data = null) {
const options = { method: 'DELETE' }
if (data) {
options.body = JSON.stringify(data)
}
return this.request(endpoint, options)
}
}
// 创建 API 客户端实例
const apiClient = new ApiClient()
// 导出API客户端实例
export default apiClient
/**
* 用户管理 API
*/
export const userApi = {
// 获取所有用户
async getAll() {
return apiClient.get('/api/users')
},
// 根据ID获取用户
async getById(userId) {
return apiClient.get(`/api/users/${userId}`)
},
// 根据手机号获取用户
async getByPhone(phone) {
return apiClient.get(`/api/users/phone/${phone}`)
},
// 创建用户
async create(userData) {
return apiClient.post('/api/users', userData)
},
// 更新用户
async update(userId, userData) {
return apiClient.put(`/api/users/${userId}`, userData)
},
// 删除用户
async delete(userId) {
return apiClient.delete(`/api/users/${userId}`)
},
// 用户登录
async login(loginData) {
return apiClient.post('/api/users/login', loginData)
},
// 刷新token
async refreshToken(refreshToken) {
return apiClient.post('/api/users/refresh', { refresh_token: refreshToken })
},
// 用户登出
async logout() {
return apiClient.post('/api/users/logout')
},
// 获取当前用户信息
async getCurrentUser() {
return apiClient.get('/api/users/me')
}
}
/**
* 机构管理 API
*/
export const institutionApi = {
// 获取所有机构
async getAll() {
return apiClient.get('/api/institutions')
},
// 根据ID获取机构
async getById(institutionId) {
return apiClient.get(`/api/institutions/${institutionId}`)
},
// 根据机构编号获取机构
async getByInstitutionId(instId) {
return apiClient.get(`/api/institutions/institution-id/${instId}`)
},
// 根据负责人ID获取机构
async getByOwner(ownerId) {
return apiClient.get(`/api/institutions/owner/${ownerId}`)
},
// 创建机构
async create(institutionData) {
return apiClient.post('/api/institutions', institutionData)
},
// 更新机构
async update(institutionId, institutionData) {
return apiClient.put(`/api/institutions/${institutionId}`, institutionData)
},
// 删除机构
async delete(institutionId) {
return apiClient.delete(`/api/institutions/${institutionId}`)
},
// 添加机构图片
async addImage(institutionId, imageData) {
return apiClient.post(`/api/institutions/${institutionId}/images`, imageData)
},
// 删除机构图片
async deleteImage(institutionId, imageId) {
return apiClient.delete(`/api/institutions/${institutionId}/images/${imageId}`)
},
// 批量创建机构
async batchCreate(institutions) {
return apiClient.post('/api/institutions/batch', { institutions })
},
// 批量删除机构
async batchDelete(institutionIds) {
return apiClient.delete('/api/institutions/batch-delete', { institution_ids: institutionIds })
}
}
/**
* 系统配置 API
*/
export const configApi = {
// 获取所有配置(键值对格式)
async getAll() {
return apiClient.get('/api/config')
},
// 获取配置列表(详细格式)
async getList() {
return apiClient.get('/api/config/list')
},
// 根据键名获取配置
async getByKey(configKey) {
return apiClient.get(`/api/config/${configKey}`)
},
// 设置配置项
async set(configKey, configValue, description = '') {
return apiClient.post('/api/config', {
config_key: configKey,
config_value: configValue,
description
})
},
// 批量更新配置
async updateMultiple(configs) {
return apiClient.put('/api/config', configs)
},
// 删除配置项
async delete(configKey) {
return apiClient.delete(`/api/config/${configKey}`)
},
// 重置为默认配置
async reset() {
return apiClient.post('/api/config/reset')
}
}
/**
* 历史数据 API
*/
export const historyApi = {
// 获取所有历史记录
async getAll() {
return apiClient.get('/api/history')
},
// 根据月份获取历史记录
async getByMonth(month) {
return apiClient.get(`/api/history/${month}`)
},
// 保存月度历史记录
async save(historyData) {
return apiClient.post('/api/history', historyData)
},
// 删除指定月份历史记录
async deleteByMonth(month) {
return apiClient.delete(`/api/history/${month}`)
},
// 获取历史统计摘要
async getSummary() {
return apiClient.get('/api/history/stats/summary')
},
// 清理旧历史数据
async cleanup(keepMonths = 12) {
return apiClient.post('/api/history/cleanup', { keep_months: keepMonths })
}
}
/**
* 数据迁移 API
*/
export const migrationApi = {
// 执行数据迁移
async migrate(migrationData) {
return apiClient.post('/api/migration/migrate', migrationData)
},
// 检查迁移状态
async checkStatus() {
return apiClient.post('/api/migration/check')
},
// 清空数据库
async clearDatabase(confirm = false) {
return apiClient.post('/api/migration/clear', { confirm })
}
}
// 删除了不必要的健康检查和连接管理器
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { userApi } from '@/services/api'
import apiClient from '@/services/api'
import { useDataStore } from './data' import { useDataStore } from './data'
/** /**
...@@ -8,7 +10,6 @@ import { useDataStore } from './data' ...@@ -8,7 +10,6 @@ import { useDataStore } from './data'
*/ */
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const currentUser = ref(null) const currentUser = ref(null)
const dataStore = useDataStore()
/** /**
* 计算属性:是否已认证 * 计算属性:是否已认证
...@@ -19,54 +20,133 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -19,54 +20,133 @@ export const useAuthStore = defineStore('auth', () => {
* 计算属性:是否为管理员 * 计算属性:是否为管理员
*/ */
const isAdmin = computed(() => currentUser.value?.role === 'admin') const isAdmin = computed(() => currentUser.value?.role === 'admin')
/**
* 登录后加载数据
*/
const loadDataAfterLogin = async () => {
try {
const dataStore = useDataStore()
console.log('📊 登录成功,开始加载数据...')
const dataLoaded = await dataStore.loadData()
if (dataLoaded) {
console.log('✅ 数据加载成功')
} else {
console.warn('⚠️ 数据加载失败,使用离线模式')
}
} catch (error) {
console.error('❌ 登录后数据加载失败:', error)
}
}
/** /**
* 用户登录 * 用户登录
* @param {string} phone - 手机号 * @param {string} phone - 手机号
* @param {string} password - 密码 * @param {string} password - 密码
* @returns {boolean} 登录是否成功 * @returns {boolean} 登录是否成功
*/ */
const login = (phone, password) => { const login = async (phone, password) => {
const users = dataStore.getUsers() try {
const user = users.find(u => u.phone === phone && u.password === password) // 使用 API 进行登录验证
const response = await userApi.login({ phone, password })
if (user) {
currentUser.value = user if (response.success && response.user && response.access_token) {
localStorage.setItem('score_system_current_user', JSON.stringify(user)) // 保存用户信息
return true currentUser.value = response.user
localStorage.setItem('currentUser', JSON.stringify(response.user))
// 保存tokens到API客户端
apiClient.saveTokens(response.access_token, response.refresh_token)
// 登录成功后加载数据
await loadDataAfterLogin()
console.log('✅ 用户登录成功:', response.user.name)
return true
}
console.log('❌ 登录失败:', response.message)
return false
} catch (error) {
console.error('登录请求失败:', error)
return false
} }
return false
} }
/** /**
* 用户登出 * 用户登出
*/ */
const logout = () => { const logout = async () => {
currentUser.value = null try {
localStorage.removeItem('score_system_current_user') // 调用后端登出接口
await userApi.logout()
} catch (error) {
console.error('登出请求失败:', error)
} finally {
// 清除本地数据
currentUser.value = null
localStorage.removeItem('currentUser')
// 清除tokens
apiClient.clearTokens()
console.log('✅ 用户已登出')
}
} }
/** /**
* 恢复登录状态 * 恢复登录状态
* 从localStorage恢复用户登录状态 * 从 localStorage 中恢复登录状态并验证token有效性
*/ */
const restoreAuth = () => { const restoreAuth = async () => {
const saved = localStorage.getItem('score_system_current_user') try {
if (saved) { // 从 localStorage 中获取用户信息
try { const savedUser = localStorage.getItem('currentUser')
const user = JSON.parse(saved) const accessToken = apiClient.getAccessToken()
// 验证用户是否仍然存在
const users = dataStore.getUsers() if (savedUser && accessToken) {
const existingUser = users.find(u => u.id === user.id) // 验证token是否有效
if (existingUser) { try {
currentUser.value = existingUser const userInfo = await userApi.getCurrentUser()
} else { if (userInfo && userInfo.id) {
logout() // 用户不存在,清除登录状态 currentUser.value = userInfo
// 恢复登录状态后加载数据
await loadDataAfterLogin()
console.log('✅ 登录状态已恢复:', userInfo.name)
return
}
} catch (error) {
console.warn('Token验证失败,尝试刷新:', error)
// Token可能过期,尝试刷新
const refreshed = await apiClient.refreshAccessToken()
if (refreshed) {
try {
const userInfo = await userApi.getCurrentUser()
if (userInfo && userInfo.id) {
currentUser.value = userInfo
localStorage.setItem('currentUser', JSON.stringify(userInfo))
// 恢复登录状态后加载数据
await loadDataAfterLogin()
console.log('✅ 登录状态已恢复(刷新后):', userInfo.name)
return
}
} catch (retryError) {
console.error('刷新后仍无法获取用户信息:', retryError)
}
}
} }
} catch (error) {
console.error('恢复登录状态失败:', error)
logout()
} }
// 恢复失败,清除数据
currentUser.value = null
localStorage.removeItem('currentUser')
apiClient.clearTokens()
console.log('🔐 未找到有效登录状态,需要重新登录')
} catch (error) {
console.error('恢复登录状态失败:', error)
currentUser.value = null
} }
} }
...@@ -77,26 +157,33 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -77,26 +157,33 @@ export const useAuthStore = defineStore('auth', () => {
const updateCurrentUser = (userData) => { const updateCurrentUser = (userData) => {
if (currentUser.value) { if (currentUser.value) {
currentUser.value = { ...currentUser.value, ...userData } currentUser.value = { ...currentUser.value, ...userData }
localStorage.setItem('score_system_current_user', JSON.stringify(currentUser.value)) console.log('✅ 当前用户信息已更新')
} }
} }
// 管理员用户切换功能(内存中保存)
const adminUser = ref(null)
/** /**
* 切换到指定用户视图(管理员功能) * 切换到指定用户视图(管理员功能)
* @param {string} userId - 要切换到的用户ID * @param {string} userId - 要切换到的用户ID
*/ */
const switchToUser = (userId) => { const switchToUser = async (userId) => {
const { useDataStore } = require('./data') try {
const dataStore = useDataStore() const { useDataStore } = await import('./data')
const user = dataStore.getUserById(userId) const dataStore = useDataStore()
const user = dataStore.getUserById(userId)
if (user && currentUser.value?.role === 'admin') {
// 保存原管理员信息 if (user && currentUser.value?.role === 'admin') {
localStorage.setItem('score_system_admin_user', JSON.stringify(currentUser.value)) // 保存原管理员信息到内存
adminUser.value = currentUser.value
// 切换到目标用户
currentUser.value = user // 切换到目标用户
localStorage.setItem('score_system_current_user', JSON.stringify(user)) currentUser.value = user
console.log('✅ 已切换到用户视图:', user.name)
}
} catch (error) {
console.error('切换用户视图失败:', error)
} }
} }
...@@ -104,11 +191,10 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -104,11 +191,10 @@ export const useAuthStore = defineStore('auth', () => {
* 从用户视图切换回管理员视图 * 从用户视图切换回管理员视图
*/ */
const switchBackToAdmin = () => { const switchBackToAdmin = () => {
const adminUser = localStorage.getItem('score_system_admin_user') if (adminUser.value) {
if (adminUser) { currentUser.value = adminUser.value
currentUser.value = JSON.parse(adminUser) adminUser.value = null
localStorage.setItem('score_system_current_user', JSON.stringify(currentUser.value)) console.log('✅ 已切换回管理员视图')
localStorage.removeItem('score_system_admin_user')
} }
} }
...@@ -121,6 +207,7 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -121,6 +207,7 @@ export const useAuthStore = defineStore('auth', () => {
restoreAuth, restoreAuth,
updateCurrentUser, updateCurrentUser,
switchToUser, switchToUser,
switchBackToAdmin switchBackToAdmin,
loadDataAfterLogin
} }
}) })
\ No newline at end of file
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import {
userApi,
institutionApi,
configApi,
historyApi,
migrationApi
} from '@/services/api'
/** /**
* 数据管理store * 数据管理store - 重构版本
* 处理用户、机构、图片上传等数据的CRUD操作 * 使用 API 服务替代 localStorage 存储
* 保持与原版本相同的接口,确保前端组件无需修改
*/ */
export const useDataStore = defineStore('data', () => { export const useDataStore = defineStore('data', () => {
// 存储键名常量
const STORAGE_KEYS = {
USERS: 'score_system_users',
INSTITUTIONS: 'score_system_institutions',
SYSTEM_CONFIG: 'score_system_config'
}
// 响应式数据 // 响应式数据
const users = ref([]) const users = ref([])
const institutions = ref([]) const institutions = ref([])
const systemConfig = ref({}) const systemConfig = ref({})
/** // 状态管理
* 初始化系统数据 const isLoading = ref(false)
* 只创建管理员用户,不创建示例机构 const isOnline = ref(true)
*/ const migrationStatus = ref({
const initializeData = () => { isCompleted: false,
// 只创建管理员用户,不创建示例用户和机构 isInProgress: false,
const defaultUsers = [ error: null
{ })
id: 'admin',
name: '系统管理员', // 删除了不必要的数据库连接检查
phone: 'admin',
password: 'admin123',
role: 'admin',
institutions: []
}
]
// 不创建默认机构,所有机构都通过管理员添加
const defaultInstitutions = []
// 保存到store和localStorage
users.value = defaultUsers
institutions.value = defaultInstitutions
systemConfig.value = {
initialized: true,
version: '8.8.0', // 系统版本 8.8
hasDefaultData: false // 标记没有默认示例数据
}
saveToStorage()
console.log('✅ 系统初始化完成,只创建了管理员用户,无示例机构')
}
/** /**
* 从localStorage加载数据 * 初始化数据库数据
*/ */
const loadFromStorage = () => { const initializeDatabaseData = async () => {
try { try {
const savedUsers = localStorage.getItem(STORAGE_KEYS.USERS) console.log('🔄 初始化数据库数据...')
const savedInstitutions = localStorage.getItem(STORAGE_KEYS.INSTITUTIONS)
const savedConfig = localStorage.getItem(STORAGE_KEYS.SYSTEM_CONFIG)
console.log('正在加载数据...')
console.log('保存的用户数据:', savedUsers ? '存在' : '不存在')
console.log('保存的机构数据:', savedInstitutions ? '存在' : '不存在')
console.log('保存的配置数据:', savedConfig ? '存在' : '不存在')
// 检查是否有任何保存的数据
const hasAnyData = savedUsers || savedInstitutions || savedConfig
if (hasAnyData) {
// 加载保存的数据
if (savedUsers) {
users.value = JSON.parse(savedUsers)
console.log(`加载了 ${users.value.length} 个用户`)
}
if (savedInstitutions) {
institutions.value = JSON.parse(savedInstitutions)
// 确保每个机构都有images数组并且数据结构正确
institutions.value.forEach(institution => {
if (!institution.images) {
institution.images = []
console.log(`为机构 ${institution.name} 初始化images数组`)
} else if (!Array.isArray(institution.images)) {
console.warn(`机构 ${institution.name} 的images不是数组,重新初始化`)
institution.images = []
} else {
// 确保每个图片对象都有必要的属性
institution.images = institution.images.filter(img => img && img.id && img.url)
console.log(`机构 ${institution.name}${institution.images.length} 张图片`)
}
})
// 额外修复:检测并修复内部ID(inst.id)重复问题,防止图片加到错误机构
const seenIds = new Set()
let duplicateFixCount = 0
institutions.value.forEach(inst => {
if (!inst.id || seenIds.has(inst.id)) {
const oldId = inst.id
// 生成全局唯一的新ID
let newId
do {
newId = `inst_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
} while (seenIds.has(newId))
inst.id = newId
duplicateFixCount++
console.warn(`🔧 修复机构内部ID重复/缺失: ${oldId || '(空)'} -> ${inst.id} (${inst.name})`)
}
seenIds.add(inst.id)
})
if (duplicateFixCount > 0) {
// 如有修复,立即保存,避免后续操作再次命中旧ID
saveToStorage()
console.log(`✅ 已自动修复 ${duplicateFixCount} 个机构的内部ID冲突`)
}
// 强制触发响应式更新 // 检查数据库是否已有数据
institutions.value = [...institutions.value] const status = await migrationApi.checkStatus()
console.log(`加载了 ${institutions.value.length} 个机构`) if (status.total_records === 0) {
} console.log('📊 数据库为空,创建默认配置...')
if (savedConfig) {
systemConfig.value = JSON.parse(savedConfig)
console.log('加载了系统配置')
}
// 如果配置显示未初始化,但有数据存在,更新配置状态 // 创建默认系统配置
if (!systemConfig.value.initialized) { await configApi.set('initialized', true)
systemConfig.value.initialized = true await configApi.set('version', '8.8.0')
systemConfig.value.version = '1.0.0' await configApi.set('hasDefaultData', false)
saveToStorage()
}
console.log('✅ 数据加载完成,使用保存的数据') console.log('✅ 默认配置创建完成')
} else {
// 没有任何保存的数据,执行首次初始化
console.log('🔄 首次启动,初始化默认数据')
initializeData()
} }
return true
} catch (error) { } catch (error) {
console.error('从localStorage加载数据失败:', error) console.error('初始化数据库数据失败:', error)
console.log('🔄 数据加载失败,重新初始化') throw error
initializeData()
}
}
/**
* 检查localStorage使用情况
*/
const getStorageUsage = () => {
let total = 0
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length
}
} }
return total
} }
/** /**
* 保存数据到localStorage * 从数据库加载数据
*/ */
const saveToStorage = () => { const loadFromDatabase = async () => {
try { try {
const usersData = JSON.stringify(users.value) isLoading.value = true
const institutionsData = JSON.stringify(institutions.value) console.log('🔄 从数据库加载数据...')
const configData = JSON.stringify(systemConfig.value)
// 并行加载所有数据
const [usersData, institutionsData, configData] = await Promise.all([
userApi.getAll(),
institutionApi.getAll(),
configApi.getAll()
])
// 转换机构数据格式(后端使用下划线,前端使用驼峰命名)
const convertedInstitutions = institutionsData.map(inst => ({
id: inst.id,
institutionId: inst.institution_id,
name: inst.name,
ownerId: inst.owner_id,
images: inst.images || [],
created_at: inst.created_at,
updated_at: inst.updated_at
}))
// 检查数据大小 // 更新本地状态
const totalSize = usersData.length + institutionsData.length + configData.length users.value = usersData
const maxSize = 5 * 1024 * 1024 // 5MB限制 institutions.value = convertedInstitutions
systemConfig.value = configData
isOnline.value = true
if (totalSize > maxSize) { console.log(`✅ 数据加载完成: ${users.value.length} 个用户, ${institutions.value.length} 个机构`)
console.warn('数据大小超出localStorage限制,可能保存失败')
// 可以在这里实现数据压缩或清理策略
}
localStorage.setItem(STORAGE_KEYS.USERS, usersData) return true
localStorage.setItem(STORAGE_KEYS.INSTITUTIONS, institutionsData)
localStorage.setItem(STORAGE_KEYS.SYSTEM_CONFIG, configData)
console.log(`数据保存成功,使用空间: ${(totalSize / 1024).toFixed(2)} KB`)
} catch (error) { } catch (error) {
console.error('保存数据到localStorage失败:', error) console.error('从数据库加载数据失败:', error)
if (error.name === 'QuotaExceededError') { isOnline.value = false
console.error('localStorage空间不足,请清理数据或减少图片上传')
// 可以触发用户提示
if (typeof window !== 'undefined' && window.ElMessage) {
window.ElMessage.error('存储空间不足,图片可能无法保存!请删除一些图片后重试。')
}
}
throw error throw error
} finally {
isLoading.value = false
} }
} }
/**
* 获取所有用户
*/
const getUsers = () => users.value
/**
* 根据ID获取用户
*/
const getUserById = (id) => users.value.find(u => u.id === id)
/**
* 添加用户
*/
const addUser = (userData) => {
const newUser = {
id: `user_${Date.now()}`,
...userData,
institutions: userData.institutions || []
}
users.value.push(newUser)
saveToStorage()
return newUser
}
/**
* 更新用户信息
*/
const updateUser = (userId, userData) => {
const index = users.value.findIndex(u => u.id === userId)
if (index !== -1) {
users.value[index] = { ...users.value[index], ...userData }
saveToStorage()
return users.value[index]
}
return null
}
/**
* 删除用户
*/
const deleteUser = (userId) => {
const index = users.value.findIndex(u => u.id === userId)
if (index !== -1) {
// 将用户的机构转移到公池(无负责人)
institutions.value.forEach(inst => {
if (inst.ownerId === userId) {
inst.ownerId = null
}
})
users.value.splice(index, 1)
saveToStorage()
return true
}
return false
}
/**
* 获取机构列表
*/
const getInstitutions = () => institutions.value
/**
* 根据用户ID获取其负责的机构(带权限验证)
*/
const getInstitutionsByUserId = (userId) => {
if (!userId) {
console.warn('getInstitutionsByUserId: userId为空')
return []
}
const userInstitutions = institutions.value.filter(inst => inst.ownerId === userId)
console.log(`用户 ${userId} 负责的机构数量: ${userInstitutions.length}`)
return userInstitutions
}
/** /**
* 安全获取机构列表(仅管理员可获取所有机构) * 初始化空数据状态
*/ */
const getInstitutionsSafely = (currentUserId, isAdmin = false) => { const initializeEmptyData = async () => {
if (isAdmin) { console.log('🔄 初始化空数据状态...')
console.log('管理员获取所有机构')
return institutions.value
} else {
console.log(`普通用户 ${currentUserId} 获取自己的机构`)
return getInstitutionsByUserId(currentUserId)
}
}
/**
* 生成下一个机构ID
*/
const generateNextInstitutionId = () => {
const existingIds = institutions.value
.map(inst => inst.institutionId)
.filter(id => id && /^\d+$/.test(id)) // 只考虑纯数字ID
.map(id => parseInt(id))
.filter(num => !isNaN(num))
const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0 users.value = []
return String(maxId + 1).padStart(3, '0') // 返回纯数字ID,如001、002 institutions.value = []
} systemConfig.value = {}
/** console.log('✅ 空数据状态初始化完成')
* 检查机构ID是否已存在
*/
const isInstitutionIdExists = (institutionId) => {
return institutions.value.some(inst => inst.institutionId === institutionId)
} }
/** /**
* 修复机构ID重复问题 * 主数据加载函数
*/ */
const fixDuplicateInstitutionIds = () => { const loadData = async () => {
console.log('开始修复机构ID重复问题...') try {
console.log('🚀 开始加载数据...')
const institutionIds = institutions.value.map(inst => inst.institutionId)
const duplicateIds = institutionIds.filter((id, index) => institutionIds.indexOf(id) !== index)
if (duplicateIds.length === 0) {
console.log('没有发现重复的机构ID')
return { fixed: 0, duplicates: [] }
}
console.log('发现重复的机构ID:', duplicateIds)
let fixedCount = 0
const fixedInstitutions = []
// 为每个重复的机构ID重新分配唯一ID
duplicateIds.forEach(duplicateId => {
const duplicateInstitutions = institutions.value.filter(inst => inst.institutionId === duplicateId)
// 保留第一个机构的ID不变,为其他机构重新分配ID
for (let i = 1; i < duplicateInstitutions.length; i++) {
const institution = duplicateInstitutions[i]
// 生成新的机构ID
let newId = parseInt(duplicateId) + 1000 + fixedCount
while (isInstitutionIdExists(String(newId).padStart(3, '0'))) {
newId++
}
const oldId = institution.institutionId
institution.institutionId = String(newId).padStart(3, '0')
console.log(`修复机构 "${institution.name}": ${oldId} -> ${institution.institutionId}`)
fixedInstitutions.push({ // 直接从数据库加载数据
name: institution.name, await loadFromDatabase()
oldId: oldId,
newId: institution.institutionId
})
fixedCount++ // 如果数据库为空,初始化基础数据
if (users.value.length === 0 && institutions.value.length === 0) {
await initializeDatabaseData()
// 重新加载数据
await loadFromDatabase()
} }
})
// 保存修复后的数据
saveToStorage()
console.log(`修复完成,共修复 ${fixedCount} 个机构`) console.log('✅ 数据加载完成')
return true
return { } catch (error) {
fixed: fixedCount, console.error('❌ 数据加载失败:', error)
duplicates: duplicateIds, // 初始化空数据状态,避免界面崩溃
fixedInstitutions: fixedInstitutions await initializeEmptyData()
return false
} }
} }
/** // 删除了不必要的兼容性函数
* 添加机构
*/
const addInstitution = (institutionData) => {
// 检查机构ID是否提供
if (!institutionData.institutionId) {
throw new Error('机构ID不能为空')
}
// 检查机构ID是否为数字 // ========== 用户管理函数 ==========
if (!/^\d+$/.test(institutionData.institutionId)) {
throw new Error('机构ID必须为数字')
}
// 检查机构ID是否重复
if (isInstitutionIdExists(institutionData.institutionId)) {
throw new Error(`机构ID ${institutionData.institutionId} 已存在`)
}
const newInstitution = {
id: `inst_${Date.now()}`,
...institutionData,
images: []
}
institutions.value.push(newInstitution)
saveToStorage()
return newInstitution
}
/** /**
* 更新机构信息 * 获取所有用户
*/
const updateInstitution = (institutionId, institutionData) => {
const index = institutions.value.findIndex(inst => inst.id === institutionId)
if (index !== -1) {
institutions.value[index] = { ...institutions.value[index], ...institutionData }
saveToStorage()
return institutions.value[index]
}
return null
}
/**
* 删除机构
*/
const deleteInstitution = (institutionId) => {
const index = institutions.value.findIndex(inst => inst.id === institutionId)
if (index !== -1) {
institutions.value.splice(index, 1)
saveToStorage()
return true
}
return false
}
/**
* 计算图片内容哈希值(用于重复检测)
*/ */
const calculateImageHash = (imageUrl) => { const getUsers = () => users.value
// 简单的哈希算法,基于图片数据的前1000个字符
const data = imageUrl.substring(0, 1000)
let hash = 0
for (let i = 0; i < data.length; i++) {
const char = data.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // 转换为32位整数
}
return Math.abs(hash).toString(36)
}
/** /**
* 检测重复图片 * 根据ID获取用户
*/ */
const detectDuplicateImage = (imageData, currentUserId = null) => { const getUserById = (id) => users.value.find(u => u.id === id)
console.log('🔍 开始检测重复图片...')
const result = {
isDuplicate: false,
duplicateType: null,
duplicateInfo: null,
allowUpload: true,
message: ''
}
// 计算当前图片的哈希值
const currentHash = calculateImageHash(imageData.url)
const currentSize = imageData.size
const currentName = imageData.name
console.log('当前图片信息:', {
name: currentName,
size: currentSize,
hash: currentHash
})
// 遍历所有机构的所有图片进行比较
for (const institution of institutions.value) {
if (!institution.images || institution.images.length === 0) continue
for (const existingImage of institution.images) {
const existingHash = calculateImageHash(existingImage.url)
const existingSize = existingImage.size
const existingName = existingImage.name
// 1. 检查完全相同的图片(内容和大小都相同)
if (currentHash === existingHash && currentSize === existingSize) {
result.isDuplicate = true
result.duplicateType = 'identical'
result.duplicateInfo = {
institutionName: institution.name,
imageName: existingName,
uploadTime: existingImage.uploadTime
}
result.allowUpload = false
result.message = `检测到完全相同的图片已存在于机构"${institution.name}"中`
console.log('❌ 发现完全相同的图片:', result.duplicateInfo)
return result
}
// 2. 检查内容相似但大小不同的图片(可能是轻微编辑)
if (currentHash === existingHash && Math.abs(currentSize - existingSize) < existingSize * 0.1) {
// 大小差异小于10%,认为是轻微编辑
result.isDuplicate = true
result.duplicateType = 'similar_edit'
result.duplicateInfo = {
institutionName: institution.name,
imageName: existingName,
uploadTime: existingImage.uploadTime,
sizeDiff: Math.abs(currentSize - existingSize)
}
result.allowUpload = true // 允许轻微编辑后的图片上传
result.message = `检测到相似图片(可能是轻微编辑),允许上传`
console.log('⚠️ 发现相似图片(轻微编辑):', result.duplicateInfo)
return result
}
// 3. 检查文件名相同但内容不同的图片
if (currentName === existingName && currentHash !== existingHash) {
result.isDuplicate = true
result.duplicateType = 'same_name_different_content'
result.duplicateInfo = {
institutionName: institution.name,
imageName: existingName,
uploadTime: existingImage.uploadTime
}
result.allowUpload = true // 允许文件名相同但内容不同的图片
result.message = `检测到同名但内容不同的图片,允许上传`
console.log('ℹ️ 发现同名不同内容图片:', result.duplicateInfo)
return result
}
}
}
console.log('✅ 未发现重复图片,允许上传')
result.message = '未发现重复图片'
return result
}
/** /**
* 为机构添加图片(带多重权限验证和重复检测) * 添加用户
*/ */
const addImageToInstitution = (institutionId, imageData, currentUserId = null, options = {}) => { const addUser = async (userData) => {
console.log('=== addImageToInstitution 开始 ===')
console.log('参数:', {
institutionId,
imageDataName: imageData.name,
imageDataSize: imageData.size,
currentUserId
})
// 🔒 第一重验证:检查用户ID是否提供
if (!currentUserId) {
console.error('❌ 安全验证失败:未提供用户ID')
throw new Error('安全验证失败:用户身份验证失败')
}
// 🔒 第二重验证:检查用户是否存在
const currentUser = users.value.find(user => user.id === currentUserId)
if (!currentUser) {
console.error('❌ 安全验证失败:用户不存在', currentUserId)
throw new Error('安全验证失败:用户不存在')
}
console.log('当前用户:', {
id: currentUser.id,
name: currentUser.name,
role: currentUser.role
})
// 🔒 第三重验证:先检查用户权限范围,只在用户自己的机构中查找
const userInstitutions = institutions.value.filter(inst => inst.ownerId === currentUserId)
console.log('用户负责的机构数量:', userInstitutions.length)
console.log('用户机构列表:', userInstitutions.map(inst => ({
id: inst.id,
name: inst.name,
institutionId: inst.institutionId
})))
// 🔍 在用户权限范围内查找目标机构(容错:若内部ID错乱,按名称和机构编号兜底匹配)
let institution = userInstitutions.find(inst => inst.id === institutionId)
if (!institution && options) {
const { expectedName, expectedInstitutionId } = options
if (expectedName) {
institution = userInstitutions.find(inst => inst.name === expectedName)
if (institution) console.warn('⚠️ 通过机构名称兜底匹配到机构:', expectedName)
}
if (!institution && expectedInstitutionId) {
institution = userInstitutions.find(inst => inst.institutionId === expectedInstitutionId)
if (institution) console.warn('⚠️ 通过机构编号兜底匹配到机构:', expectedInstitutionId)
}
}
if (!institution) {
console.error('❌ 权限验证失败:机构不存在或不属于当前用户')
console.error('查找的机构ID:', institutionId)
console.error('用户ID:', currentUserId)
console.error('用户名:', currentUser.name)
console.error('用户可访问的机构:', userInstitutions.map(inst => ({
id: inst.id,
name: inst.name
})))
// 检查是否存在该机构但属于其他用户
const globalInstitution = institutions.value.find(inst => inst.id === institutionId)
if (globalInstitution) {
const actualOwner = users.value.find(user => user.id === globalInstitution.ownerId)
console.error('🚨 安全警告:尝试访问其他用户的机构!')
console.error('🚨 目标机构:', globalInstitution.name)
console.error('🚨 实际负责人:', actualOwner ? actualOwner.name : '未知')
console.error('🚨 尝试访问的用户:', currentUser.name)
throw new Error(`🚨 严重安全错误:您无权操作机构"${globalInstitution.name}",此事件已被记录`)
} else {
throw new Error(`机构不存在: ${institutionId}`)
}
}
console.log('✅ 权限验证通过,找到目标机构:', {
id: institution.id,
name: institution.name,
institutionId: institution.institutionId,
ownerId: institution.ownerId
})
// 🔍 重复图片检测
console.log('🔍 开始重复图片检测...')
const duplicateCheck = detectDuplicateImage(imageData, currentUserId)
if (duplicateCheck.isDuplicate && !duplicateCheck.allowUpload) {
console.error('❌ 重复图片检测失败:', duplicateCheck.message)
throw new Error(duplicateCheck.message)
}
if (duplicateCheck.isDuplicate && duplicateCheck.allowUpload) {
console.warn('⚠️ 重复图片检测警告:', duplicateCheck.message)
// 继续上传,但记录警告信息
}
// 确保机构有images数组
if (!institution.images) {
institution.images = []
console.log(`为机构 ${institution.name} 初始化images数组`)
} else if (!Array.isArray(institution.images)) {
console.warn(`机构 ${institution.name} 的images不是数组,重新初始化`)
institution.images = []
}
console.log(`机构 ${institution.name} 当前图片数量: ${institution.images.length}`)
console.log('当前图片列表:', institution.images.map(img => ({ id: img.id, name: img.name })))
if (institution.images.length >= 10) {
console.warn(`机构 ${institution.name} 图片数量已达上限: ${institution.images.length}`)
return null
}
const newImage = {
id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
...imageData,
uploadTime: new Date().toISOString(),
hash: calculateImageHash(imageData.url),
duplicateCheck: duplicateCheck.isDuplicate ? {
type: duplicateCheck.duplicateType,
info: duplicateCheck.duplicateInfo,
message: duplicateCheck.message
} : null
}
console.log('创建新图片对象:', newImage)
// 确保响应式更新 - 创建新的数组引用
const oldImagesLength = institution.images.length
institution.images = [...institution.images, newImage]
console.log(`图片数组更新: ${oldImagesLength} -> ${institution.images.length}`)
// 强制触发响应式更新
const oldInstitutionsLength = institutions.value.length
institutions.value = [...institutions.value]
console.log(`机构数组更新: ${oldInstitutionsLength} -> ${institutions.value.length}`)
try { try {
saveToStorage() const newUserData = {
console.log(`✅ 图片添加成功: ${newImage.name}, 机构: ${institution.name}, 当前图片数量: ${institution.images.length}`) id: `user_${Date.now()}`,
...userData,
// 验证保存结果 institutions: userData.institutions || []
const savedData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]') }
const savedInstitution = savedData.find(inst => inst.id === institutionId)
console.log('localStorage验证:', { const newUser = await userApi.create(newUserData)
找到机构: !!savedInstitution,
图片数量: savedInstitution?.images?.length || 0 // 更新本地状态
}) users.value.push(newUser)
console.log('=== addImageToInstitution 完成 ===') return newUser
return newImage
} catch (error) { } catch (error) {
console.error('❌ 保存数据失败:', error) console.error('添加用户失败:', error)
// 回滚操作
institution.images = institution.images.filter(img => img.id !== newImage.id)
institutions.value = [...institutions.value]
console.log('=== addImageToInstitution 回滚 ===')
throw error throw error
} }
} }
/**
* 从机构删除图片(带权限验证)
*/
const removeImageFromInstitution = (institutionId, imageId, currentUserId = null) => {
console.log('=== removeImageFromInstitution 开始 ===')
console.log('参数:', { institutionId, imageId, currentUserId })
// 🔒 权限验证:检查用户ID
if (!currentUserId) {
console.error('❌ 安全验证失败:未提供用户ID')
throw new Error('安全验证失败:用户身份验证失败')
}
// 🔒 权限验证:检查用户是否存在
const currentUser = users.value.find(user => user.id === currentUserId)
if (!currentUser) {
console.error('❌ 安全验证失败:用户不存在', currentUserId)
throw new Error('安全验证失败:用户不存在')
}
// 🔒 权限验证:只在用户自己的机构中查找
const userInstitutions = institutions.value.filter(inst => inst.ownerId === currentUserId)
const institution = userInstitutions.find(inst => inst.id === institutionId)
if (!institution) {
console.error('❌ 权限验证失败:机构不存在或不属于当前用户')
console.error('查找的机构ID:', institutionId)
console.error('用户ID:', currentUserId)
// 检查是否存在该机构但属于其他用户
const globalInstitution = institutions.value.find(inst => inst.id === institutionId)
if (globalInstitution) {
const actualOwner = users.value.find(user => user.id === globalInstitution.ownerId)
console.error('🚨 安全警告:尝试删除其他用户机构的图片!')
console.error('🚨 目标机构:', globalInstitution.name)
console.error('🚨 实际负责人:', actualOwner ? actualOwner.name : '未知')
throw new Error(`🚨 严重安全错误:您无权操作机构"${globalInstitution.name}"的图片`)
}
throw new Error('机构不存在或您无权访问')
}
console.log('找到目标机构:', institution.name)
console.log('当前图片数量:', institution.images.length)
// 查找要删除的图片
const imageIndex = institution.images.findIndex(img => img.id === imageId)
if (imageIndex === -1) {
console.warn('图片不存在:', imageId)
throw new Error('要删除的图片不存在')
}
const imageToDelete = institution.images[imageIndex]
console.log('找到要删除的图片:', imageToDelete.name)
try {
// 删除图片 - 创建新的数组引用确保响应式更新
const oldImagesLength = institution.images.length
institution.images = institution.images.filter(img => img.id !== imageId)
console.log(`图片数组更新: ${oldImagesLength} -> ${institution.images.length}`)
// 强制触发响应式更新
const oldInstitutionsLength = institutions.value.length
institutions.value = [...institutions.value]
console.log(`机构数组更新: ${oldInstitutionsLength} -> ${institutions.value.length}`)
// 保存到localStorage
saveToStorage()
// 验证删除结果
const savedData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]')
const savedInstitution = savedData.find(inst => inst.id === institutionId)
console.log('localStorage验证:', {
找到机构: !!savedInstitution,
图片数量: savedInstitution?.images?.length || 0
})
console.log(`✅ 图片删除成功: ${imageToDelete.name}, 机构: ${institution.name}, 剩余图片数量: ${institution.images.length}`)
console.log('=== removeImageFromInstitution 完成 ===')
return true
} catch (error) {
console.error('❌ 删除图片失败:', error)
console.log('=== removeImageFromInstitution 失败 ===')
throw error
}
}
/**
* 计算用户的互动得分
*/
const calculateInteractionScore = (userId) => {
const userInstitutions = getInstitutionsByUserId(userId)
let totalScore = 0
userInstitutions.forEach(inst => {
const imageCount = inst.images.length
if (imageCount === 0) {
totalScore += 0
} else if (imageCount === 1) {
totalScore += 0.5
} else {
totalScore += 1
}
})
return totalScore
}
/**
* 计算用户的绩效得分
*/
const calculatePerformanceScore = (userId) => {
const userInstitutions = getInstitutionsByUserId(userId)
const institutionCount = userInstitutions.length
if (institutionCount === 0) return 0
const interactionScore = calculateInteractionScore(userId)
return (interactionScore / institutionCount) * 10
}
/** /**
* 获取所有用户的得分统计 * 更新用户信息
*/
const getAllUserScores = computed(() => {
return users.value
.filter(user => user.role === 'user')
.map(user => ({
...user,
institutionCount: getInstitutionsByUserId(user.id).length,
interactionScore: calculateInteractionScore(user.id),
performanceScore: calculatePerformanceScore(user.id)
}))
})
/**
* 清空所有数据(重置系统)
*/ */
const clearAllData = () => { const updateUser = async (userId, userData) => {
try { try {
localStorage.removeItem(STORAGE_KEYS.USERS) const updatedUser = await userApi.update(userId, userData)
localStorage.removeItem(STORAGE_KEYS.INSTITUTIONS)
localStorage.removeItem(STORAGE_KEYS.SYSTEM_CONFIG) // 更新本地状态
const index = users.value.findIndex(u => u.id === userId)
users.value = [] if (index !== -1) {
institutions.value = [] users.value[index] = updatedUser
systemConfig.value = {} }
console.log('✅ 所有数据已清空') return updatedUser
return true
} catch (error) { } catch (error) {
console.error('清空数据失败:', error) console.error('更新用户失败:', error)
return false throw error
} }
} }
/** /**
* 重置为默认数据(只保留管理员) * 删除用户
*/ */
const resetToDefault = () => { const deleteUser = async (userId) => {
try { try {
clearAllData() await userApi.delete(userId)
initializeData()
console.log('✅ 系统已重置,只保留管理员用户') // 更新本地状态
const index = users.value.findIndex(u => u.id === userId)
if (index !== -1) {
users.value.splice(index, 1)
}
return true return true
} catch (error) { } catch (error) {
console.error('重置数据失败:', error) console.error('删除用户失败:', error)
return false throw error
}
}
/**
* 清理示例数据,只保留真实机构
*/
const cleanupExampleData = () => {
console.log('开始清理示例数据...')
// 移除示例用户(保留管理员和真实用户)
const exampleUserIds = ['user1', 'user2', 'user3']
const realUsers = users.value.filter(user => !exampleUserIds.includes(user.id))
// 移除示例机构(名称为单个字符或符号的机构)
const exampleInstitutionNames = ['A', 'B', 'C', 'D', 'E', 'a', 'b', 'c', 'd', 'e', '①', '②', '③', '④', '⑤']
const realInstitutions = institutions.value.filter(inst => !exampleInstitutionNames.includes(inst.name))
const removedUsers = users.value.length - realUsers.length
const removedInstitutions = institutions.value.length - realInstitutions.length
users.value = realUsers
institutions.value = realInstitutions
saveToStorage()
console.log(`清理完成:移除了 ${removedUsers} 个示例用户,${removedInstitutions} 个示例机构`)
return {
removedUsers,
removedInstitutions,
remainingUsers: realUsers.length,
remainingInstitutions: realInstitutions.length
}
}
/**
* 修复机构数据结构
*/
const fixInstitutionDataStructure = () => {
console.log('开始修复机构数据结构...')
let fixedCount = 0
institutions.value.forEach(institution => {
let needsFix = false
// 确保有images数组
if (!institution.images) {
institution.images = []
needsFix = true
console.log(`为机构 ${institution.name} 添加images数组`)
} else if (!Array.isArray(institution.images)) {
institution.images = []
needsFix = true
console.log(`修复机构 ${institution.name} 的images数组类型`)
} else {
// 清理无效的图片数据
const validImages = institution.images.filter(img => img && img.id && img.url)
if (validImages.length !== institution.images.length) {
institution.images = validImages
needsFix = true
console.log(`清理机构 ${institution.name} 的无效图片数据`)
}
}
// 确保有必要的属性
if (!institution.id) {
institution.id = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
needsFix = true
console.log(`为机构 ${institution.name} 生成ID`)
}
if (!institution.institutionId) {
institution.institutionId = generateNextInstitutionId()
needsFix = true
console.log(`为机构 ${institution.name} 生成机构ID: ${institution.institutionId}`)
}
// 确保有负责人ID
if (!institution.ownerId) {
console.warn(`机构 ${institution.name} 没有负责人,将设为无负责人状态`)
institution.ownerId = null
needsFix = true
}
if (needsFix) {
fixedCount++
}
})
if (fixedCount > 0) {
// 强制触发响应式更新
institutions.value = [...institutions.value]
saveToStorage()
console.log(`修复完成:共修复了 ${fixedCount} 个机构的数据结构`)
} else {
console.log('所有机构数据结构正常,无需修复')
}
return {
fixed: fixedCount,
total: institutions.value.length
}
}
/**
* 修复数据归属问题
*/
const fixDataOwnership = () => {
console.log('开始修复数据归属问题...')
let fixedCount = 0
const issues = []
// 检查机构归属
institutions.value.forEach(institution => {
// 检查负责人是否存在
if (institution.ownerId) {
const owner = users.value.find(user => user.id === institution.ownerId)
if (!owner) {
console.warn(`机构 ${institution.name} 的负责人 ${institution.ownerId} 不存在`)
issues.push({
type: 'orphan_institution',
institutionName: institution.name,
ownerId: institution.ownerId
})
// 将机构设为无负责人状态
institution.ownerId = null
fixedCount++
}
}
// 检查图片归属
if (institution.images && institution.images.length > 0) {
institution.images.forEach((image, index) => {
if (!image.id || !image.url) {
console.warn(`机构 ${institution.name} 的第 ${index + 1} 张图片数据不完整`)
issues.push({
type: 'invalid_image',
institutionName: institution.name,
imageIndex: index,
imageData: image
})
}
})
// 清理无效图片
const validImages = institution.images.filter(img => img && img.id && img.url)
if (validImages.length !== institution.images.length) {
institution.images = validImages
fixedCount++
}
}
})
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`数据归属修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('数据归属正常,无需修复')
}
return {
fixed: fixedCount,
issues: issues,
total: institutions.value.length
}
}
/**
* 紧急修复特定用户数据泄露问题
*/
const emergencyFixDataLeak = () => {
console.log('🚨 开始紧急修复数据泄露问题...')
let fixedCount = 0
const fixedIssues = []
// 查找目标用户
const chenRuiPing = users.value.find(user => user.name === '陈锐屏')
const yuFangFei = users.value.find(user => user.name === '余芳菲' || user.name === '余芳飞')
if (!chenRuiPing || !yuFangFei) {
console.error('❌ 无法找到目标用户')
return { fixed: 0, issues: ['无法找到目标用户'] }
}
console.log('找到目标用户:', {
陈锐屏: chenRuiPing.id,
余芳菲: yuFangFei.id
})
// 修复机构归属
institutions.value.forEach(institution => {
let needsFix = false
// 检查五华区长青口腔诊所
if (institution.name.includes('五华区长青口腔诊所') || institution.name.includes('长青口腔')) {
if (institution.ownerId !== chenRuiPing.id) {
console.log(`🔧 修复机构归属: ${institution.name} ${institution.ownerId} -> ${chenRuiPing.id}`)
institution.ownerId = chenRuiPing.id
fixedIssues.push(`修复机构 ${institution.name} 归属到陈锐屏`)
needsFix = true
}
}
// 检查大连西岗悦佳口腔诊所
if (institution.name.includes('大连西岗悦佳口腔诊所') || institution.name.includes('悦佳口腔')) {
if (institution.ownerId !== yuFangFei.id) {
console.log(`🔧 修复机构归属: ${institution.name} ${institution.ownerId} -> ${yuFangFei.id}`)
institution.ownerId = yuFangFei.id
fixedIssues.push(`修复机构 ${institution.name} 归属到余芳菲`)
needsFix = true
}
}
if (needsFix) {
fixedCount++
}
})
// 检查并修复图片归属错误
const wuHuaQu = institutions.value.find(inst =>
inst.name.includes('五华区长青口腔诊所') && inst.ownerId === chenRuiPing.id
)
const dalianXiGang = institutions.value.find(inst =>
inst.name.includes('大连西岗悦佳口腔诊所') && inst.ownerId === yuFangFei.id
)
// 如果大连西岗机构中有不属于余芳菲的图片,移动到正确的机构
if (dalianXiGang && dalianXiGang.images && dalianXiGang.images.length > 0) {
const suspiciousImages = []
const validImages = []
dalianXiGang.images.forEach(img => {
// 检查图片上传时间和其他特征,判断是否可能是错误归属的图片
if (img.name && (img.name.includes('长青') || img.uploadTime)) {
// 这可能是错误归属的图片
suspiciousImages.push(img)
console.log(`🔧 发现可疑图片: ${img.name}${dalianXiGang.name}`)
} else {
validImages.push(img)
}
})
if (suspiciousImages.length > 0 && wuHuaQu) {
// 将可疑图片移动到正确的机构
if (!wuHuaQu.images) wuHuaQu.images = []
wuHuaQu.images.push(...suspiciousImages)
dalianXiGang.images = validImages
fixedIssues.push(`移动 ${suspiciousImages.length} 张图片从 ${dalianXiGang.name}${wuHuaQu.name}`)
fixedCount++
}
}
if (fixedCount > 0) {
// 强制触发响应式更新
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🚨 紧急修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('✅ 未发现需要紧急修复的问题')
}
return {
fixed: fixedCount,
issues: fixedIssues,
chenRuiPingId: chenRuiPing.id,
yuFangFeiId: yuFangFei.id
}
}
/**
* 修复图片归属错误问题
*/
const fixImageOwnershipIssues = () => {
console.log('🔧 开始修复图片归属错误问题...')
let fixedCount = 0
const fixedIssues = []
// 检查特定的问题机构
const problemInstitutions = [
{ keywords: ['五华区长青口腔诊所', '长青口腔'], expectedOwner: '陈锐屏' },
{ keywords: ['昆明市五华区爱雅仕口腔诊所', '爱雅仕口腔'], expectedOwner: '陈锐屏' },
{ keywords: ['昆明美云口腔医院有限公司安宁口腔诊所', '美云口腔', '安宁口腔'], expectedOwner: null },
{ keywords: ['兰州至善振林康美口腔医疗有限责任公司', '至善振林', '康美口腔'], expectedOwner: null }
]
problemInstitutions.forEach(problem => {
const matchingInstitutions = institutions.value.filter(inst =>
problem.keywords.some(keyword => inst.name.includes(keyword))
)
if (matchingInstitutions.length > 0) {
console.log(`发现问题机构组: ${problem.keywords[0]}`)
matchingInstitutions.forEach(inst => {
console.log(` - ${inst.name} (ID: ${inst.institutionId}, 负责人: ${inst.ownerId})`)
if (problem.expectedOwner) {
const expectedUser = users.value.find(user => user.name === problem.expectedOwner)
if (expectedUser && inst.ownerId !== expectedUser.id) {
console.log(` 🔧 修复归属: ${inst.ownerId} -> ${expectedUser.id}`)
inst.ownerId = expectedUser.id
fixedIssues.push(`修复机构 ${inst.name} 归属到 ${expectedUser.name}`)
fixedCount++
}
}
})
// 检查是否有图片归属错误
if (matchingInstitutions.length > 1) {
console.log(` ⚠️ 发现多个相似机构,可能存在图片归属混乱`)
// 合并相似机构的图片到正确的机构
const primaryInstitution = matchingInstitutions[0]
const secondaryInstitutions = matchingInstitutions.slice(1)
secondaryInstitutions.forEach(secondary => {
if (secondary.images && secondary.images.length > 0) {
console.log(` 🔧 移动 ${secondary.images.length} 张图片从 ${secondary.name}${primaryInstitution.name}`)
if (!primaryInstitution.images) primaryInstitution.images = []
primaryInstitution.images.push(...secondary.images)
secondary.images = []
fixedIssues.push(`移动图片从 ${secondary.name}${primaryInstitution.name}`)
fixedCount++
}
})
}
}
})
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🔧 图片归属修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现图片归属问题')
}
return {
fixed: fixedCount,
issues: fixedIssues
}
}
/**
* 修复图片上传权限验证错误
*/
const fixImageUploadPermissionErrors = () => {
console.log('🔒 开始修复图片上传权限验证错误...')
let fixedCount = 0
const fixedIssues = []
// 1. 检查机构ID重复问题
console.log('1️⃣ 检查机构ID重复问题...')
const institutionIdMap = new Map()
const internalIdMap = new Map()
institutions.value.forEach(institution => {
// 检查机构ID重复
if (institution.institutionId) {
if (institutionIdMap.has(institution.institutionId)) {
const existing = institutionIdMap.get(institution.institutionId)
console.error(`🚨 发现重复机构ID: ${institution.institutionId}`)
console.error(` - 机构1: ${existing.name} (内部ID: ${existing.id}, 负责人: ${existing.ownerId})`)
console.error(` - 机构2: ${institution.name} (内部ID: ${institution.id}, 负责人: ${institution.ownerId})`)
// 为重复的机构生成新的ID
const newId = generateNextInstitutionId()
console.log(` 🔧 为机构 ${institution.name} 分配新ID: ${newId}`)
institution.institutionId = newId
fixedIssues.push(`修复重复机构ID: ${institution.name} -> ${newId}`)
fixedCount++
} else {
institutionIdMap.set(institution.institutionId, institution)
}
}
// 检查内部ID重复
if (institution.id) {
if (internalIdMap.has(institution.id)) {
const existing = internalIdMap.get(institution.id)
console.error(`🚨 发现重复内部ID: ${institution.id}`)
console.error(` - 机构1: ${existing.name}`)
console.error(` - 机构2: ${institution.name}`)
// 为重复的机构生成新的内部ID
const newInternalId = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(` 🔧 为机构 ${institution.name} 分配新内部ID: ${newInternalId}`)
institution.id = newInternalId
fixedIssues.push(`修复重复内部ID: ${institution.name} -> ${newInternalId}`)
fixedCount++
} else {
internalIdMap.set(institution.id, institution)
}
}
})
// 2. 检查特定的权限混乱问题
console.log('2️⃣ 检查特定的权限混乱问题...')
// 检查张田田的机构归属
const zhangTianTian = users.value.find(user => user.name === '张田田')
if (zhangTianTian) {
console.log('检查张田田的机构归属...')
// 查找武夷山思美达口腔门诊部
const wuyishanInst = institutions.value.find(inst =>
inst.name.includes('武夷山思美达') || inst.name.includes('思美达')
)
if (wuyishanInst) {
console.log('找到武夷山思美达口腔门诊部:', wuyishanInst.name)
console.log('当前负责人ID:', wuyishanInst.ownerId)
if (wuyishanInst.ownerId !== zhangTianTian.id) {
console.log(`🔧 修复武夷山思美达口腔门诊部归属: ${wuyishanInst.ownerId} -> ${zhangTianTian.id}`)
wuyishanInst.ownerId = zhangTianTian.id
fixedIssues.push(`修复武夷山思美达口腔门诊部归属到张田田`)
fixedCount++
}
}
// 检查崇川区海虹口腔门诊部是否错误归属给张田田
const chongchuanInst = institutions.value.find(inst =>
inst.name.includes('崇川区海虹') || inst.name.includes('海虹')
)
if (chongchuanInst && chongchuanInst.ownerId === zhangTianTian.id) {
console.log('发现崇川区海虹口腔门诊部错误归属给张田田')
console.log(`🔧 移除崇川区海虹口腔门诊部的错误归属`)
chongchuanInst.ownerId = null // 设为公池
fixedIssues.push(`移除崇川区海虹口腔门诊部的错误归属`)
fixedCount++
}
}
// 3. 保存修复结果
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🔒 图片上传权限验证修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现需要修复的图片上传权限问题')
}
return {
fixed: fixedCount,
issues: fixedIssues
} }
} }
/** // ========== 机构管理函数 ==========
* 修复机构权限验证错误(原有函数保持不变)
*/
const fixInstitutionPermissionErrors = () => {
console.log('🔒 开始修复机构权限验证错误...')
let fixedCount = 0
const fixedIssues = []
// 1. 检查机构ID重复问题
console.log('1️⃣ 检查机构ID重复问题...')
const institutionIdMap = new Map()
const internalIdMap = new Map()
institutions.value.forEach(institution => {
// 检查机构ID重复
if (institution.institutionId) {
if (institutionIdMap.has(institution.institutionId)) {
const existing = institutionIdMap.get(institution.institutionId)
console.error(`🚨 发现重复机构ID: ${institution.institutionId}`)
console.error(` - 机构1: ${existing.name} (内部ID: ${existing.id}, 负责人: ${existing.ownerId})`)
console.error(` - 机构2: ${institution.name} (内部ID: ${institution.id}, 负责人: ${institution.ownerId})`)
// 为重复的机构生成新的ID
const newId = generateNextInstitutionId()
console.log(` 🔧 为机构 ${institution.name} 分配新ID: ${newId}`)
institution.institutionId = newId
fixedIssues.push(`修复重复机构ID: ${institution.name} -> ${newId}`)
fixedCount++
} else {
institutionIdMap.set(institution.institutionId, institution)
}
}
// 检查内部ID重复
if (institution.id) {
if (internalIdMap.has(institution.id)) {
const existing = internalIdMap.get(institution.id)
console.error(`🚨 发现重复内部ID: ${institution.id}`)
console.error(` - 机构1: ${existing.name}`)
console.error(` - 机构2: ${institution.name}`)
// 为重复的机构生成新的内部ID
const newInternalId = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(` 🔧 为机构 ${institution.name} 分配新内部ID: ${newInternalId}`)
institution.id = newInternalId
fixedIssues.push(`修复重复内部ID: ${institution.name} -> ${newInternalId}`)
fixedCount++
} else {
internalIdMap.set(institution.id, institution)
}
}
})
// 2. 检查特定的问题机构
console.log('2️⃣ 检查特定问题机构...')
const problemCases = [
{
userKeywords: ['昆明美云口腔医院', '安宁口腔'],
institutionKeywords: ['昆明美云口腔医院有限公司安宁口腔诊所', '美云口腔', '安宁口腔'],
conflictKeywords: ['兰州至善振林康美口腔医疗有限责任公司', '至善振林', '康美口腔'],
description: '昆明美云口腔医院安宁口腔诊所权限错误'
},
{
userKeywords: ['五华区长青口腔', '长青口腔'],
institutionKeywords: ['五华区长青口腔诊所', '长青口腔'],
conflictKeywords: ['昆明市五华区爱雅仕口腔诊所', '爱雅仕口腔'],
description: '五华区长青口腔诊所权限错误'
}
]
problemCases.forEach(problemCase => {
console.log(`检查问题: ${problemCase.description}`)
// 查找相关机构
const targetInstitutions = institutions.value.filter(inst =>
problemCase.institutionKeywords.some(keyword => inst.name.includes(keyword))
)
const conflictInstitutions = institutions.value.filter(inst =>
problemCase.conflictKeywords.some(keyword => inst.name.includes(keyword))
)
if (targetInstitutions.length > 0 && conflictInstitutions.length > 0) {
console.log(` 发现问题机构组:`)
console.log(` 目标机构: ${targetInstitutions.map(i => i.name).join(', ')}`)
console.log(` 冲突机构: ${conflictInstitutions.map(i => i.name).join(', ')}`)
// 检查是否有权限交叉
targetInstitutions.forEach(target => {
conflictInstitutions.forEach(conflict => {
if (target.ownerId === conflict.ownerId && target.ownerId) {
console.warn(` ⚠️ 发现权限交叉: 用户 ${target.ownerId} 同时负责 ${target.name}${conflict.name}`)
fixedIssues.push(`发现权限交叉: ${target.name}${conflict.name}`)
}
})
})
}
})
// 3. 检查用户权限映射
console.log('3️⃣ 检查用户权限映射...')
users.value.forEach(user => {
if (user.role === 'user') {
const userInstitutions = institutions.value.filter(inst => inst.ownerId === user.id)
if (userInstitutions.length > 0) {
console.log(`用户 ${user.name} (${user.id}) 负责 ${userInstitutions.length} 个机构:`)
userInstitutions.forEach(inst => {
console.log(` - ${inst.name} (机构ID: ${inst.institutionId}, 内部ID: ${inst.id})`)
})
// 检查是否有跨地区的机构归属(可能的权限错误)
const regions = new Set()
userInstitutions.forEach(inst => {
if (inst.name.includes('五华区') || inst.name.includes('昆明')) {
regions.add('昆明')
} else if (inst.name.includes('大连')) {
regions.add('大连')
} else if (inst.name.includes('兰州')) {
regions.add('兰州')
} else if (inst.name.includes('安宁')) {
regions.add('安宁')
} else if (inst.name.includes('北京')) {
regions.add('北京')
} else if (inst.name.includes('上海')) {
regions.add('上海')
}
})
if (regions.size > 1) {
console.warn(`⚠️ 用户 ${user.name} 负责跨地区机构: ${Array.from(regions).join(', ')}`)
fixedIssues.push(`用户 ${user.name} 负责跨地区机构: ${Array.from(regions).join(', ')}`)
}
// 检查是否有机构名称相似但归属不同的情况
const institutionNames = userInstitutions.map(inst => inst.name)
institutionNames.forEach(name => {
const similarInstitutions = institutions.value.filter(inst =>
inst.name.includes(name.split('口腔')[0]) && inst.ownerId !== user.id
)
if (similarInstitutions.length > 0) {
console.warn(`⚠️ 发现相似机构名称但归属不同:`)
console.warn(` 用户机构: ${name}`)
similarInstitutions.forEach(similar => {
console.warn(` 相似机构: ${similar.name} (归属: ${similar.ownerId})`)
})
fixedIssues.push(`发现相似机构名称但归属不同: ${name}`)
}
})
}
}
})
// 4. 验证localStorage数据一致性
console.log('4️⃣ 验证localStorage数据一致性...')
try {
const savedData = localStorage.getItem('score_system_institutions')
if (savedData) {
const savedInstitutions = JSON.parse(savedData)
// 检查内存数据和localStorage数据是否一致
if (savedInstitutions.length !== institutions.value.length) {
console.warn(`⚠️ 数据不一致: 内存中 ${institutions.value.length} 个机构,localStorage中 ${savedInstitutions.length} 个机构`)
fixedIssues.push(`数据不一致: 内存和localStorage机构数量不匹配`)
}
// 检查具体的机构数据
institutions.value.forEach(memInst => {
const savedInst = savedInstitutions.find(si => si.id === memInst.id)
if (!savedInst) {
console.warn(`⚠️ 机构 ${memInst.name} 在localStorage中不存在`)
fixedIssues.push(`机构 ${memInst.name} 在localStorage中不存在`)
} else if (savedInst.ownerId !== memInst.ownerId) {
console.warn(`⚠️ 机构 ${memInst.name} 的负责人不一致: 内存(${memInst.ownerId}) vs localStorage(${savedInst.ownerId})`)
fixedIssues.push(`机构 ${memInst.name} 负责人数据不一致`)
}
})
}
} catch (error) {
console.error('检查localStorage数据时出错:', error)
fixedIssues.push('localStorage数据检查失败')
}
if (fixedCount > 0) {
institutions.value = [...institutions.value]
saveToStorage()
console.log(`🔒 权限验证修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现需要修复的权限验证问题')
}
return {
fixed: fixedCount,
issues: fixedIssues
}
}
/** /**
* 测试图片上传权限验证 * 获取所有机构
*/ */
const testImageUploadPermissions = (userId) => { const getInstitutions = () => institutions.value
console.log('🧪 开始测试图片上传权限验证...')
const user = users.value.find(u => u.id === userId)
if (!user) {
console.error('❌ 用户不存在:', userId)
return { success: false, error: '用户不存在' }
}
console.log(`测试用户: ${user.name} (${user.id})`)
const userInstitutions = institutions.value.filter(inst => inst.ownerId === userId)
console.log(`用户负责的机构数量: ${userInstitutions.length}`)
const testResults = []
userInstitutions.forEach(institution => {
console.log(`\n测试机构: ${institution.name}`)
console.log(` - 机构ID: ${institution.institutionId}`)
console.log(` - 内部ID: ${institution.id}`)
console.log(` - 负责人: ${institution.ownerId}`)
try {
// 模拟图片数据
const mockImageData = {
id: `test_${Date.now()}`,
name: `测试图片_${institution.name}.jpg`,
url: 'data:image/jpeg;base64,test',
size: 1024,
uploadTime: new Date().toISOString()
}
// 测试权限验证逻辑(不实际添加图片)
console.log(' 🔒 开始权限验证测试...')
// 第一重验证:检查用户ID
if (!userId) {
throw new Error('用户ID验证失败')
}
// 第二重验证:检查用户是否存在 /**
const currentUser = users.value.find(user => user.id === userId) * 根据用户ID获取机构
if (!currentUser) { */
throw new Error('用户不存在') const getInstitutionsByUserId = (userId) => {
} return institutions.value.filter(inst => inst.ownerId === userId)
}
// 第三重验证:检查机构是否存在 /**
const inst = institutions.value.find(inst => inst.id === institution.id) * 安全获取机构(兼容性函数)
if (!inst) { */
throw new Error('机构不存在') const getInstitutionsSafely = () => {
} return institutions.value.filter(inst => inst && inst.id)
}
// 第四重验证:检查权限 /**
if (inst.ownerId !== userId) { * 添加机构
throw new Error(`权限验证失败: 机构负责人(${inst.ownerId}) != 当前用户(${userId})`) */
} const addInstitution = async (institutionData) => {
try {
// 检查机构ID是否提供
if (!institutionData.institutionId) {
throw new Error('机构ID不能为空')
}
// 第五重验证:双重确认机构归属 // 检查机构ID是否为数字
const userInsts = institutions.value.filter(inst => inst.ownerId === userId) if (!/^\d+$/.test(institutionData.institutionId)) {
const isUserInst = userInsts.some(inst => inst.id === institution.id) throw new Error('机构ID必须为数字')
if (!isUserInst) { }
throw new Error('双重验证失败: 机构不在用户机构列表中')
}
console.log(' ✅ 权限验证测试通过') // 检查机构ID是否重复
testResults.push({ if (isInstitutionIdExists(institutionData.institutionId)) {
institutionId: institution.id, throw new Error(`机构ID ${institutionData.institutionId} 已存在`)
institutionName: institution.name, }
success: true,
message: '权限验证通过'
})
} catch (error) { const newInstitutionData = {
console.error(` ❌ 权限验证测试失败: ${error.message}`) id: `inst_${Date.now()}`,
testResults.push({ institution_id: institutionData.institutionId,
institutionId: institution.id, name: institutionData.name,
institutionName: institution.name, owner_id: institutionData.ownerId
success: false,
error: error.message
})
} }
})
// 测试其他用户的机构(应该失败) const newInstitution = await institutionApi.create(newInstitutionData)
console.log('\n🧪 测试访问其他用户机构(应该失败)...')
const otherInstitutions = institutions.value.filter(inst => inst.ownerId !== userId).slice(0, 3)
otherInstitutions.forEach(institution => { // 转换为前端格式
console.log(`测试无权访问的机构: ${institution.name}`) const frontendInstitution = {
try { id: newInstitution.id,
if (institution.ownerId !== userId) { institutionId: newInstitution.institution_id,
throw new Error(`权限验证失败: 无权操作机构"${institution.name}"`) name: newInstitution.name,
} ownerId: newInstitution.owner_id,
console.error(' ❌ 安全漏洞: 权限验证应该失败但却通过了!') images: newInstitution.images || []
testResults.push({
institutionId: institution.id,
institutionName: institution.name,
success: false,
error: '安全漏洞: 权限验证应该失败但却通过了'
})
} catch (error) {
console.log(` ✅ 正确拒绝: ${error.message}`)
testResults.push({
institutionId: institution.id,
institutionName: institution.name,
success: true,
message: '正确拒绝无权访问'
})
} }
})
const successCount = testResults.filter(r => r.success).length // 更新本地状态
const totalCount = testResults.length institutions.value.push(frontendInstitution)
console.log(`\n🧪 权限验证测试完成: ${successCount}/${totalCount} 通过`) return frontendInstitution
} catch (error) {
return { console.error('添加机构失败:', error)
success: successCount === totalCount, throw error
userId: userId,
userName: user.name,
totalTests: totalCount,
passedTests: successCount,
results: testResults
} }
} }
/** /**
* 修复虚拟机构和真实机构ID问题 * 更新机构信息
*/ */
const fixVirtualRealInstitutionIds = () => { const updateInstitution = async (institutionId, institutionData) => {
console.log('🔧 开始修复虚拟机构和真实机构ID问题...') try {
const updateData = {
let fixedCount = 0 name: institutionData.name,
const fixedIssues = [] institution_id: institutionData.institutionId,
owner_id: institutionData.ownerId
// 1. 检查并修复缺失的ID
console.log('1️⃣ 检查并修复缺失的ID...')
institutions.value.forEach(institution => {
let needsFix = false
// 确保有内部ID
if (!institution.id) {
institution.id = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(`为机构 ${institution.name} 生成内部ID: ${institution.id}`)
fixedIssues.push(`为机构 ${institution.name} 生成内部ID`)
needsFix = true
} }
// 确保有机构编号 const updatedInstitution = await institutionApi.update(institutionId, updateData)
if (!institution.institutionId) {
institution.institutionId = generateNextInstitutionId()
console.log(`为机构 ${institution.name} 生成机构编号: ${institution.institutionId}`)
fixedIssues.push(`为机构 ${institution.name} 生成机构编号`)
needsFix = true
}
// 确保有负责人ID(如果没有,设为null) // 转换为前端格式并更新本地状态
if (institution.ownerId === undefined) { const index = institutions.value.findIndex(inst => inst.id === institutionId)
institution.ownerId = null if (index !== -1) {
console.log(`机构 ${institution.name} 设置为无负责人状态`) institutions.value[index] = {
fixedIssues.push(`机构 ${institution.name} 设置为无负责人状态`) id: updatedInstitution.id,
needsFix = true institutionId: updatedInstitution.institution_id,
name: updatedInstitution.name,
ownerId: updatedInstitution.owner_id,
images: updatedInstitution.images || []
}
} }
// 确保有images数组 return institutions.value[index]
if (!institution.images) { } catch (error) {
institution.images = [] console.error('更新机构失败:', error)
console.log(`为机构 ${institution.name} 初始化images数组`) throw error
fixedIssues.push(`为机构 ${institution.name} 初始化images数组`) }
needsFix = true }
}
if (needsFix) { /**
fixedCount++ * 删除机构
} */
}) const deleteInstitution = async (institutionId) => {
try {
// 2. 检查并修复ID格式问题 await institutionApi.delete(institutionId)
console.log('2️⃣ 检查并修复ID格式问题...')
institutions.value.forEach(institution => {
// 检查机构编号格式(应该是纯数字,如001、002)
if (institution.institutionId && !/^\d{3}$/.test(institution.institutionId)) {
const oldId = institution.institutionId
institution.institutionId = generateNextInstitutionId()
console.log(`修复机构 ${institution.name} 的编号格式: ${oldId} -> ${institution.institutionId}`)
fixedIssues.push(`修复机构 ${institution.name} 的编号格式`)
fixedCount++
}
// 检查内部ID格式(应该是inst_开头) // 更新本地状态
if (institution.id && !institution.id.startsWith('inst_')) { const index = institutions.value.findIndex(inst => inst.id === institutionId)
const oldId = institution.id if (index !== -1) {
institution.id = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` institutions.value.splice(index, 1)
console.log(`修复机构 ${institution.name} 的内部ID格式: ${oldId} -> ${institution.id}`)
fixedIssues.push(`修复机构 ${institution.name} 的内部ID格式`)
fixedCount++
}
})
// 3. 检查并修复负责人关联
console.log('3️⃣ 检查并修复负责人关联...')
institutions.value.forEach(institution => {
if (institution.ownerId) {
const owner = users.value.find(user => user.id === institution.ownerId)
if (!owner) {
console.warn(`机构 ${institution.name} 的负责人 ${institution.ownerId} 不存在,设为无负责人状态`)
institution.ownerId = null
fixedIssues.push(`机构 ${institution.name} 的负责人不存在,已清理`)
fixedCount++
}
} }
})
// 4. 验证修复结果 return true
console.log('4️⃣ 验证修复结果...') } catch (error) {
const validationErrors = [] console.error('删除机构失败:', error)
throw error
}
}
institutions.value.forEach(institution => { /**
if (!institution.id) { * 批量添加机构
validationErrors.push(`机构 ${institution.name} 仍然缺少内部ID`) */
} const batchAddInstitutions = async (institutionsData) => {
if (!institution.institutionId) { try {
validationErrors.push(`机构 ${institution.name} 仍然缺少机构编号`) // 准备批量创建的数据
} const institutionsToCreate = institutionsData.map(inst => ({
if (!Array.isArray(institution.images)) { id: `inst_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
validationErrors.push(`机构 ${institution.name} 的images不是数组`) institution_id: inst.institutionId,
} name: inst.name,
}) owner_id: inst.ownerId
}))
if (validationErrors.length > 0) { // 调用批量创建API
console.error('验证失败:', validationErrors) const result = await institutionApi.batchCreate(institutionsToCreate)
fixedIssues.push(...validationErrors)
} else {
console.log('✅ 验证通过,所有机构数据结构正确')
}
if (fixedCount > 0) { // 重新加载数据以获取最新状态
institutions.value = [...institutions.value] await loadFromDatabase()
saveToStorage()
console.log(`🔧 虚拟/真实机构ID修复完成:共修复了 ${fixedCount} 个问题`)
} else {
console.log('未发现需要修复的ID问题')
}
return { return result
fixed: fixedCount, } catch (error) {
issues: fixedIssues console.error('批量添加机构失败:', error)
throw error
} }
} }
/** /**
* 诊断图片上传问题 * 批量删除机构
*/ */
const diagnoseImageUploadIssue = (userId, institutionInternalId) => { const batchDeleteInstitutions = async (institutionIds) => {
console.log('🔍 诊断图片上传问题...') try {
console.log(`用户ID: ${userId}`) // 调用批量删除API
console.log(`机构内部ID: ${institutionInternalId}`) const result = await institutionApi.batchDelete(institutionIds)
const diagnosticResult = {
success: false,
issues: [],
suggestions: []
}
// 1. 检查用户是否存在
const user = users.value.find(u => u.id === userId)
if (!user) {
diagnosticResult.issues.push('用户不存在')
diagnosticResult.suggestions.push('检查用户ID是否正确')
return diagnosticResult
}
console.log(`找到用户: ${user.name} (${user.role})`)
// 2. 检查机构是否存在
const institution = institutions.value.find(i => i.id === institutionInternalId)
if (!institution) {
diagnosticResult.issues.push('机构不存在')
diagnosticResult.suggestions.push('检查机构内部ID是否正确')
// 尝试通过机构编号查找 // 更新本地状态
const instByNumber = institutions.value.find(i => i.institutionId === institutionInternalId) institutionIds.forEach(id => {
if (instByNumber) { const index = institutions.value.findIndex(inst => inst.id === id)
diagnosticResult.suggestions.push(`可能混淆了机构编号(${institutionInternalId})和内部ID(${instByNumber.id})`) if (index !== -1) {
} institutions.value.splice(index, 1)
}
})
return diagnosticResult return result
} catch (error) {
console.error('批量删除机构失败:', error)
throw error
} }
}
console.log(`找到机构: ${institution.name}`) /**
console.log(`机构编号: ${institution.institutionId}`) * 为机构添加图片
console.log(`机构负责人: ${institution.ownerId}`) */
const addImageToInstitution = async (institutionId, imageData) => {
try {
const imageCreateData = {
id: imageData.id,
url: imageData.url,
upload_time: imageData.uploadTime || new Date().toISOString()
}
// 3. 检查权限 await institutionApi.addImage(institutionId, imageCreateData)
if (institution.ownerId !== userId) {
diagnosticResult.issues.push(`权限不匹配: 机构负责人(${institution.ownerId}) != 当前用户(${userId})`)
const actualOwner = users.value.find(u => u.id === institution.ownerId) // 更新本地状态
if (actualOwner) { const institution = institutions.value.find(inst => inst.id === institutionId)
diagnosticResult.suggestions.push(`机构 ${institution.name} 的实际负责人是 ${actualOwner.name}`) if (institution) {
} else { if (!institution.images) {
diagnosticResult.suggestions.push('机构没有有效的负责人,需要重新分配') institution.images = []
}
institution.images.push({
id: imageData.id,
url: imageData.url,
uploadTime: imageData.uploadTime || new Date().toISOString()
})
} }
return diagnosticResult return true
} } catch (error) {
console.error('添加图片失败:', error)
// 4. 检查机构数据结构 throw error
if (!institution.images) {
diagnosticResult.issues.push('机构缺少images数组')
diagnosticResult.suggestions.push('执行数据结构修复')
return diagnosticResult
} }
}
if (!Array.isArray(institution.images)) { /**
diagnosticResult.issues.push('机构的images不是数组') * 从机构删除图片
diagnosticResult.suggestions.push('执行数据结构修复') */
return diagnosticResult const removeImageFromInstitution = async (institutionId, imageId) => {
} try {
await institutionApi.deleteImage(institutionId, imageId)
// 5. 检查用户机构列表 // 更新本地状态
const userInstitutions = institutions.value.filter(inst => inst.ownerId === userId) const institution = institutions.value.find(inst => inst.id === institutionId)
const isInUserList = userInstitutions.some(inst => inst.id === institutionInternalId) if (institution && institution.images) {
const imageIndex = institution.images.findIndex(img => img.id === imageId)
if (imageIndex !== -1) {
institution.images.splice(imageIndex, 1)
}
}
if (!isInUserList) { return true
diagnosticResult.issues.push('机构不在用户的机构列表中') } catch (error) {
diagnosticResult.suggestions.push('检查机构归属关系') console.error('删除图片失败:', error)
return diagnosticResult throw error
} }
}
console.log('✅ 所有检查通过,应该可以正常上传图片') // ========== 工具函数 ==========
diagnosticResult.success = true
return diagnosticResult /**
* 检查机构ID是否存在
*/
const isInstitutionIdExists = (institutionId) => {
return institutions.value.some(inst => inst.institutionId === institutionId)
} }
/** /**
* 全面数据完整性检查和修复 * 生成下一个机构ID
*/ */
const comprehensiveDataIntegrityCheck = () => { const generateNextInstitutionId = () => {
console.log('🔍 开始全面数据完整性检查...') const existingIds = institutions.value
.map(inst => parseInt(inst.institutionId))
const report = { .filter(id => !isNaN(id))
timestamp: new Date().toISOString(), .sort((a, b) => a - b)
issues: [],
fixes: [], let nextId = 1
statistics: { for (const id of existingIds) {
totalUsers: users.value.length, if (id === nextId) {
totalInstitutions: institutions.value.length, nextId++
totalImages: 0, } else {
orphanInstitutions: 0, break
duplicateIds: 0,
invalidImages: 0
} }
} }
// 1. 检查用户数据完整性 return nextId.toString().padStart(3, '0')
console.log('1️⃣ 检查用户数据完整性...') }
users.value.forEach(user => {
if (!user.id || !user.name) {
report.issues.push(`用户数据不完整: ${JSON.stringify(user)}`)
}
})
// 2. 检查机构归属
console.log('2️⃣ 检查机构归属...')
institutions.value.forEach(institution => {
if (institution.ownerId) {
const owner = users.value.find(user => user.id === institution.ownerId)
if (!owner) {
report.issues.push(`机构 "${institution.name}" 的负责人 ${institution.ownerId} 不存在`)
report.statistics.orphanInstitutions++
}
} else {
report.statistics.orphanInstitutions++
}
// 统计图片数量 // 删除了前端的图片重复检测,这应该在后端处理
if (institution.images && Array.isArray(institution.images)) {
report.statistics.totalImages += institution.images.length
// 检查图片数据完整性 // ========== 计分系统 ==========
institution.images.forEach((img, index) => {
if (!img.id || !img.url) {
report.issues.push(`机构 "${institution.name}" 的第 ${index + 1} 张图片数据不完整`)
report.statistics.invalidImages++
}
})
}
})
// 3. 检查机构ID重复 /**
console.log('3️⃣ 检查机构ID重复...') * 计算用户的交互得分
const institutionIds = institutions.value.map(inst => inst.institutionId).filter(id => id) */
const duplicateIds = institutionIds.filter((id, index) => institutionIds.indexOf(id) !== index) const calculateInteractionScore = (userId) => {
report.statistics.duplicateIds = duplicateIds.length const userInstitutions = getInstitutionsByUserId(userId)
if (duplicateIds.length > 0) { if (userInstitutions.length === 0) return 0
report.issues.push(`发现重复的机构ID: ${duplicateIds.join(', ')}`)
}
// 4. 检查跨用户数据泄露 let totalScore = 0
console.log('4️⃣ 检查跨用户数据泄露...')
const userGroups = {}
institutions.value.forEach(inst => {
if (inst.ownerId) {
if (!userGroups[inst.ownerId]) {
userGroups[inst.ownerId] = []
}
userGroups[inst.ownerId].push(inst)
}
})
// 检查是否有用户的机构数据异常 for (const institution of userInstitutions) {
Object.keys(userGroups).forEach(userId => { const imageCount = institution.images ? institution.images.length : 0
const user = users.value.find(u => u.id === userId)
const userInstitutions = userGroups[userId]
if (user && userInstitutions.length > 0) { // 基础分数:每个机构10分
// 检查是否有异常的机构名称组合 let institutionScore = 10
const institutionNames = userInstitutions.map(inst => inst.name)
// 如果一个用户同时负责包含"五华区"和"大连"的机构,可能存在数据泄露 // 图片加分:每张图片2分,最多20分
const hasWuHua = institutionNames.some(name => name.includes('五华区')) const imageBonus = Math.min(imageCount * 2, 20)
const hasDalian = institutionNames.some(name => name.includes('大连')) institutionScore += imageBonus
if (hasWuHua && hasDalian) { totalScore += institutionScore
report.issues.push(`用户 ${user.name} 同时负责五华区和大连的机构,可能存在数据泄露`)
}
}
})
// 5. 生成修复建议
console.log('5️⃣ 生成修复建议...')
if (report.issues.length > 0) {
report.fixes.push('建议执行紧急修复功能')
report.fixes.push('建议执行数据归属修复')
report.fixes.push('建议执行机构数据结构修复')
} }
// 6. 计算数据完整性评分 return totalScore
let score = 100 }
score -= report.statistics.orphanInstitutions * 10
score -= report.statistics.duplicateIds * 15
score -= report.statistics.invalidImages * 5
score -= report.issues.filter(issue => issue.includes('数据泄露')).length * 30
report.integrityScore = Math.max(0, score) /**
* 计算用户的绩效得分
*/
const calculatePerformanceScore = (userId) => {
const userInstitutions = getInstitutionsByUserId(userId)
const institutionCount = userInstitutions.length
console.log('📊 数据完整性检查完成') if (institutionCount === 0) return 0
console.log('报告:', report)
return report const interactionScore = calculateInteractionScore(userId)
return (interactionScore / institutionCount) * 10
} }
/** /**
* 历史统计数据管理 * 获取所有用户的得分统计
*/ */
const getAllUserScores = computed(() => {
return users.value
.filter(user => user.role === 'user')
.map(user => ({
...user,
institutionCount: getInstitutionsByUserId(user.id).length,
interactionScore: calculateInteractionScore(user.id),
performanceScore: calculatePerformanceScore(user.id)
}))
})
// 历史统计数据存储键 // ========== 历史数据管理 ==========
const HISTORY_STORAGE_KEY = 'score_system_history'
/** /**
* 保存当前月份的统计数据到历史记录 * 保存当前月份的统计数据到历史记录
*/ */
const saveCurrentMonthStats = () => { const saveCurrentMonthStats = async () => {
try { try {
const currentDate = new Date() const currentDate = new Date()
const monthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}` const monthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
...@@ -2007,25 +565,19 @@ export const useDataStore = defineStore('data', () => { ...@@ -2007,25 +565,19 @@ export const useDataStore = defineStore('data', () => {
} }
}) })
// 获取现有历史数据 const historyData = {
const historyData = JSON.parse(localStorage.getItem(HISTORY_STORAGE_KEY) || '{}')
// 保存当前月份数据
historyData[monthKey] = {
month: monthKey, month: monthKey,
saveTime: new Date().toISOString(), save_time: new Date().toISOString(),
totalUsers: currentStats.length, total_users: currentStats.length,
totalInstitutions: institutions.value.length, total_institutions: institutions.value.length,
totalImages: institutions.value.reduce((total, inst) => total + (inst.images ? inst.images.length : 0), 0), total_images: institutions.value.reduce((total, inst) => total + (inst.images ? inst.images.length : 0), 0),
userStats: currentStats user_stats: currentStats
} }
// 保存到localStorage // 保存到数据库
localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(historyData)) await historyApi.save(historyData)
console.log(`✅ ${monthKey} 月份统计数据保存成功`) console.log(`✅ ${monthKey} 月份统计数据保存成功`)
console.log('保存的数据:', historyData[monthKey])
return true return true
} catch (error) { } catch (error) {
console.error('保存历史统计数据失败:', error) console.error('保存历史统计数据失败:', error)
...@@ -2036,198 +588,86 @@ export const useDataStore = defineStore('data', () => { ...@@ -2036,198 +588,86 @@ export const useDataStore = defineStore('data', () => {
/** /**
* 获取历史统计数据 * 获取历史统计数据
*/ */
const getHistoryStats = () => { const getHistoryStats = async () => {
try { try {
const historyData = JSON.parse(localStorage.getItem(HISTORY_STORAGE_KEY) || '{}') return await historyApi.getAll()
return historyData
} catch (error) { } catch (error) {
console.error('获取历史统计数据失败:', error) console.error('获取历史统计数据失败:', error)
return {} return []
} }
} }
/** /**
* 获取可用的历史月份列表 * 获取可用的历史月份
*/ */
const getAvailableHistoryMonths = () => { const getAvailableHistoryMonths = async () => {
const historyData = getHistoryStats() try {
return Object.keys(historyData).sort((a, b) => b.localeCompare(a)) // 按时间倒序 const historyData = await historyApi.getAll()
return historyData.map(item => item.month).sort().reverse()
} catch (error) {
console.error('获取历史月份失败:', error)
return []
}
} }
/** /**
* 获取指定月份的统计数据 * 获取指定月份的统计数据
*/ */
const getMonthStats = (monthKey) => { const getMonthStats = async (month) => {
const historyData = getHistoryStats()
return historyData[monthKey] || null
}
/**
* 删除指定月份的历史数据
*/
const deleteMonthStats = (monthKey) => {
try { try {
const historyData = getHistoryStats() return await historyApi.getByMonth(month)
if (historyData[monthKey]) {
delete historyData[monthKey]
localStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(historyData))
console.log(`✅ 删除 ${monthKey} 月份数据成功`)
return true
}
return false
} catch (error) { } catch (error) {
console.error('删除历史数据失败:', error) console.error('获取月份统计数据失败:', error)
return false return null
} }
} }
/** /**
* 清空所有历史数据 * 删除指定月份的统计数据
*/ */
const clearAllHistoryStats = () => { const deleteMonthStats = async (month) => {
try { try {
localStorage.removeItem(HISTORY_STORAGE_KEY) await historyApi.deleteByMonth(month)
console.log('✅ 所有历史数据已清空')
return true return true
} catch (error) { } catch (error) {
console.error('清空历史数据失败:', error) console.error('删除月份统计数据失败:', error)
return false return false
} }
} }
/** // ========== 数据管理 ==========
* 自动保存月度统计(在每月1号自动执行)
*/
const autoSaveMonthlyStats = () => {
const currentDate = new Date()
const lastSaveKey = 'last_monthly_save'
const lastSave = localStorage.getItem(lastSaveKey)
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
// 检查是否需要保存(每月只保存一次)
if (!lastSave || lastSave !== currentMonthKey) {
console.log('🔄 执行自动月度统计保存...')
const success = saveCurrentMonthStats()
if (success) {
localStorage.setItem(lastSaveKey, currentMonthKey)
}
return success
}
return false // 本月已保存过
}
/** /**
* 月度重置功能 - 每月1日自动执行 * 清空所有数据(重置系统)
*/ */
const performMonthlyReset = () => { const clearAllData = async () => {
try { try {
const currentDate = new Date() await migrationApi.clearDatabase(true)
const lastResetKey = 'last_monthly_reset'
const lastReset = localStorage.getItem(lastResetKey)
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
// 检查是否需要重置(每月只重置一次)
if (!lastReset || lastReset !== currentMonthKey) {
console.log('🔄 执行月度重置...')
// 1. 先保存当前月份的统计数据
const saveSuccess = saveCurrentMonthStats()
if (!saveSuccess) {
console.warn('⚠️ 保存月度统计失败,但继续执行重置')
}
// 2. 清空所有机构的图片上传记录
institutions.value.forEach(institution => {
if (institution.images && institution.images.length > 0) {
console.log(`清空机构 ${institution.name}${institution.images.length} 张图片`)
institution.images = []
}
})
// 3. 保存重置后的数据
saveToStorage()
// 4. 记录重置时间
localStorage.setItem(lastResetKey, currentMonthKey)
localStorage.setItem('last_reset_time', new Date().toISOString())
console.log(`✅ ${currentMonthKey} 月度重置完成`)
console.log('- 已保存上月统计数据到历史记录')
console.log('- 已清空所有机构的图片上传记录')
console.log('- 用户分数将自动重新计算')
return true users.value = []
} institutions.value = []
systemConfig.value = {}
return false // 本月已重置过 console.log('✅ 所有数据已清空')
return true
} catch (error) { } catch (error) {
console.error('月度重置失败:', error) console.error('清空数据失败:', error)
return false return false
} }
} }
/** /**
* 检查并执行月度重置(系统启动时调用) * 重置为默认数据(只保留管理员)
*/
const checkAndPerformMonthlyReset = () => {
const currentDate = new Date()
// 只在每月1-3日检查是否需要重置(给一些缓冲时间)
if (currentDate.getDate() <= 3) {
console.log('🔍 检查是否需要执行月度重置...')
return performMonthlyReset()
}
return false
}
/**
* 手动执行月度重置(管理员功能)
*/ */
const manualMonthlyReset = () => { const resetToDefault = async () => {
try { try {
console.log('🔄 手动执行月度重置...') await clearAllData()
await initializeDatabaseData()
// 1. 保存当前统计数据 console.log('✅ 系统已重置,只保留管理员用户')
const saveSuccess = saveCurrentMonthStats() return true
if (!saveSuccess) {
throw new Error('保存月度统计失败')
}
// 2. 清空所有机构的图片
let clearedCount = 0
institutions.value.forEach(institution => {
if (institution.images && institution.images.length > 0) {
clearedCount += institution.images.length
institution.images = []
}
})
// 3. 保存数据
try {
saveToStorage()
} catch (saveError) {
console.error('保存数据失败:', saveError)
throw new Error('保存重置后的数据失败')
}
// 4. 更新重置记录
const currentDate = new Date()
const currentMonthKey = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
try {
localStorage.setItem('last_monthly_reset', currentMonthKey)
localStorage.setItem('last_reset_time', new Date().toISOString())
} catch (storageError) {
console.error('保存重置记录失败:', storageError)
// 这个错误不应该阻止重置成功,因为数据已经清空了
}
console.log(`✅ 手动月度重置完成,清空了 ${clearedCount} 张图片`)
return { success: true, clearedCount }
} catch (error) { } catch (error) {
console.error('手动月度重置失败:', error) console.error('重置数据失败:', error)
return { success: false, error: error.message } return false
} }
} }
...@@ -2253,15 +693,17 @@ export const useDataStore = defineStore('data', () => { ...@@ -2253,15 +693,17 @@ export const useDataStore = defineStore('data', () => {
/** /**
* 导入数据(用于恢复) * 导入数据(用于恢复)
*/ */
const importData = (jsonData) => { const importData = async (jsonData) => {
try { try {
const data = JSON.parse(jsonData) const data = JSON.parse(jsonData)
if (data.users && data.institutions && data.systemConfig) { if (data.users && data.institutions && data.systemConfig) {
// 这里需要实现批量导入到数据库的逻辑
// 暂时先更新本地状态
users.value = data.users users.value = data.users
institutions.value = data.institutions institutions.value = data.institutions
systemConfig.value = data.systemConfig systemConfig.value = data.systemConfig
saveToStorage()
console.log('✅ 数据导入成功') console.log('✅ 数据导入成功')
return true return true
} else { } else {
...@@ -2273,157 +715,64 @@ export const useDataStore = defineStore('data', () => { ...@@ -2273,157 +715,64 @@ export const useDataStore = defineStore('data', () => {
} }
} }
/** // ========== 兼容性函数 ==========
* 修复图片显示错乱问题 // 这些函数保持与原版本相同的接口,确保前端组件无需修改
*/
const fixImageDisplayIssues = () => {
console.log('🔧 开始修复图片显示错乱问题...')
let fixedCount = 0
const issues = []
// 1. 检查机构ID冲突
const institutionIdMap = new Map()
const duplicateIds = []
institutions.value.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.length)
duplicateIds.forEach(dup => {
console.log(`重复ID: ${dup.id}`)
// 为重复的机构生成新的ID
dup.institutions.slice(1).forEach(inst => {
const oldId = inst.id
inst.id = `inst_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
console.log(`🔧 修复机构ID: ${oldId} -> ${inst.id} (${inst.name})`)
fixedCount++
})
})
issues.push(`修复了 ${duplicateIds.length} 个重复的机构ID`)
}
// 2. 检查图片数据完整性
institutions.value.forEach(inst => {
if (!inst.images) {
inst.images = []
console.log(`🔧 为机构 ${inst.name} 初始化images数组`)
fixedCount++
} else if (!Array.isArray(inst.images)) {
console.log(`🔧 修复机构 ${inst.name} 的images数据类型`)
inst.images = []
fixedCount++
} else {
// 清理无效的图片数据
const validImages = inst.images.filter(img => img && img.id && img.url && img.name)
if (validImages.length !== inst.images.length) {
const removedCount = inst.images.length - validImages.length
console.log(`🔧 清理机构 ${inst.name}${removedCount} 个无效图片`)
inst.images = validImages
fixedCount++
}
}
})
// 3. 强制触发响应式更新
institutions.value = [...institutions.value]
// 4. 保存修复后的数据
if (fixedCount > 0) {
saveToStorage()
console.log(`✅ 图片显示问题修复完成,共修复 ${fixedCount} 个问题`)
} else {
console.log('✅ 未发现图片显示问题')
}
return {
fixed: fixedCount,
issues: issues
}
}
/**
* 综合修复图片相关问题
*/
const fixAllImageIssues = () => {
console.log('🔧 开始综合修复图片相关问题...')
const results = {
permissionFix: fixImageUploadPermissionErrors(),
displayFix: fixImageDisplayIssues(),
totalFixed: 0
}
results.totalFixed = results.permissionFix.fixed + results.displayFix.fixed
console.log(`✅ 综合修复完成,共修复 ${results.totalFixed} 个问题`)
return results
}
// ========== 导出所有函数 ==========
return { return {
// 响应式数据
users, users,
institutions, institutions,
systemConfig, systemConfig,
initializeData, isLoading,
loadFromStorage, isOnline,
saveToStorage, migrationStatus,
// 核心函数
loadData,
loadFromDatabase,
initializeDatabaseData,
initializeEmptyData,
// 用户管理
getUsers, getUsers,
getUserById, getUserById,
addUser, addUser,
updateUser, updateUser,
deleteUser, deleteUser,
// 机构管理
getInstitutions, getInstitutions,
getInstitutionsByUserId, getInstitutionsByUserId,
getInstitutionsSafely, getInstitutionsSafely,
addInstitution, addInstitution,
updateInstitution, updateInstitution,
deleteInstitution, deleteInstitution,
batchAddInstitutions,
batchDeleteInstitutions,
addImageToInstitution, addImageToInstitution,
removeImageFromInstitution, removeImageFromInstitution,
calculateImageHash,
detectDuplicateImage, // 工具函数
isInstitutionIdExists,
generateNextInstitutionId,
// 计分系统
calculateInteractionScore, calculateInteractionScore,
calculatePerformanceScore, calculatePerformanceScore,
getAllUserScores, getAllUserScores,
generateNextInstitutionId,
isInstitutionIdExists, // 历史数据
fixDuplicateInstitutionIds,
cleanupExampleData,
fixInstitutionDataStructure,
fixDataOwnership,
emergencyFixDataLeak,
fixImageOwnershipIssues,
fixImageUploadPermissionErrors,
fixInstitutionPermissionErrors,
testImageUploadPermissions,
fixVirtualRealInstitutionIds,
diagnoseImageUploadIssue,
comprehensiveDataIntegrityCheck,
fixImageDisplayIssues,
fixAllImageIssues,
clearAllData,
resetToDefault,
exportData,
importData,
saveCurrentMonthStats, saveCurrentMonthStats,
getHistoryStats, getHistoryStats,
getAvailableHistoryMonths, getAvailableHistoryMonths,
getMonthStats, getMonthStats,
deleteMonthStats, deleteMonthStats,
clearAllHistoryStats,
autoSaveMonthlyStats, // 数据管理
performMonthlyReset, clearAllData,
checkAndPerformMonthlyReset, resetToDefault,
manualMonthlyReset exportData,
importData
} }
}) })
\ No newline at end of file
...@@ -55,62 +55,33 @@ export const cleanupAllNonAdminData = async () => { ...@@ -55,62 +55,33 @@ export const cleanupAllNonAdminData = async () => {
} }
/** /**
* 清理浏览器缓存 * 清理浏览器缓存(数据库模式)
*/ */
export const clearBrowserCache = () => { export const clearBrowserCache = () => {
try { try {
// 清理localStorage中的相关数据 // 数据库模式下只清理 sessionStorage 和其他临时缓存
const keysToKeep = ['score_system_users', 'score_system_institutions', 'score_system_config'] sessionStorage.clear()
// 清理可能残留的旧版本 localStorage 数据
const keysToRemove = [] const keysToRemove = []
// 找出所有相关的localStorage键
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i) const key = localStorage.key(i)
if (key && key.startsWith('score_system_') && !keysToKeep.includes(key)) { if (key && key.startsWith('score_system_')) {
keysToRemove.push(key) keysToRemove.push(key)
} }
} }
// 删除不需要的键
keysToRemove.forEach(key => { keysToRemove.forEach(key => {
localStorage.removeItem(key) localStorage.removeItem(key)
}) })
// 清理sessionStorage console.log('✅ 浏览器缓存已清理(数据库模式)')
sessionStorage.clear()
console.log('浏览器缓存已清理')
} catch (error) { } catch (error) {
console.error('清理浏览器缓存失败:', 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
}
}
/** /**
* 导出当前数据(用于备份) * 导出当前数据(用于备份)
...@@ -162,20 +133,19 @@ export const resetSystemToInitialState = async () => { ...@@ -162,20 +133,19 @@ export const resetSystemToInitialState = async () => {
const dataStore = useDataStore() const dataStore = useDataStore()
// 重置到默认状态 // 重置到默认状态
const success = dataStore.resetToDefault() const success = await dataStore.resetToDefault()
if (success) { if (success) {
ElMessage.success('系统重置完成') ElMessage.success('系统重置完成')
// 清理所有缓存 // 清理浏览器缓存
localStorage.clear() clearBrowserCache()
sessionStorage.clear()
// 刷新页面 // 刷新页面
setTimeout(() => { setTimeout(() => {
window.location.href = '/' window.location.href = '/'
}, 1500) }, 1500)
return true return true
} else { } else {
ElMessage.error('系统重置失败') ElMessage.error('系统重置失败')
......
...@@ -106,59 +106,41 @@ export const deepClone = (obj) => { ...@@ -106,59 +106,41 @@ export const deepClone = (obj) => {
} }
/** /**
* 本地存储工具 * 数据存储工具(已迁移到数据库)
* 保留接口兼容性,但实际不再使用本地存储
*/ */
export const storage = { export const storage = {
/** /**
* 获取存储数据 * 获取存储数据(已废弃)
* @param {string} key - 存储键 * @deprecated 已迁移到数据库存储,请使用相应的 API
* @param {any} defaultValue - 默认值
* @returns {any} 存储的数据
*/ */
get(key, defaultValue = null) { get(key, defaultValue = null) {
try { console.warn('storage.get 已废弃,请使用数据库 API')
const value = localStorage.getItem(key) return defaultValue
return value ? JSON.parse(value) : defaultValue
} catch (error) {
console.error('读取本地存储失败:', error)
return defaultValue
}
}, },
/** /**
* 设置存储数据 * 设置存储数据(已废弃)
* @param {string} key - 存储键 * @deprecated 已迁移到数据库存储,请使用相应的 API
* @param {any} value - 要存储的数据
*/ */
set(key, value) { set(key, value) {
try { console.warn('storage.set 已废弃,请使用数据库 API')
localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error('设置本地存储失败:', error)
}
}, },
/** /**
* 删除存储数据 * 删除存储数据(已废弃)
* @param {string} key - 存储键 * @deprecated 已迁移到数据库存储,请使用相应的 API
*/ */
remove(key) { remove(key) {
try { console.warn('storage.remove 已废弃,请使用数据库 API')
localStorage.removeItem(key)
} catch (error) {
console.error('删除本地存储失败:', error)
}
}, },
/** /**
* 清空所有存储 * 清空所有存储(已废弃)
* @deprecated 已迁移到数据库存储,请使用相应的 API
*/ */
clear() { clear() {
try { console.warn('storage.clear 已废弃,请使用数据库 API')
localStorage.clear()
} catch (error) {
console.error('清空本地存储失败:', error)
}
} }
} }
......
/**
* 服务器端数据同步管理器
* 解决跨浏览器数据同步问题
*/
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
...@@ -188,17 +188,15 @@ ...@@ -188,17 +188,15 @@
<div class="section-header"> <div class="section-header">
<h3>机构管理</h3> <h3>机构管理</h3>
<div class="section-actions"> <div class="section-actions">
<el-button @click="showBatchAddDialog">批量添加机构</el-button>
<el-upload <el-upload
class="upload-excel" class="upload-excel"
:show-file-list="false" :show-file-list="false"
:before-upload="beforeUploadExcel" :before-upload="beforeUploadExcel"
accept=".xlsx,.xls" accept=".xlsx,.xls"
style="display: inline-block; margin-left: 10px;"
> >
<el-button type="success"> <el-button type="success">
<el-icon><Upload /></el-icon> <el-icon><Upload /></el-icon>
上传表格 上传表格批量添加
</el-button> </el-button>
</el-upload> </el-upload>
<el-button type="primary" @click="showAddInstitutionDialog"> <el-button type="primary" @click="showAddInstitutionDialog">
...@@ -686,33 +684,6 @@ ...@@ -686,33 +684,6 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 批量添加机构对话框 -->
<el-dialog v-model="batchAddDialogVisible" title="批量添加机构" width="600px">
<div class="batch-add-content">
<p>请在下方文本框中输入机构信息,每行一个,格式:机构ID 机构名称(机构ID必须为数字):</p>
<el-input
v-model="batchAddText"
type="textarea"
:rows="8"
placeholder="请输入机构信息,每行一个&#10;格式:机构ID 机构名称&#10;例如:&#10;001 机构A&#10;002 机构B&#10;003 机构C&#10;注意:机构ID必须为数字,用空格分隔"
/>
<el-form-item label="默认负责人" style="margin-top: 15px">
<el-select v-model="batchAddOwnerId" style="width: 100%" clearable>
<el-option label="公池(无负责人)" :value="null" />
<el-option
v-for="user in regularUsers"
:key="user.id"
:label="user.name"
:value="user.id"
/>
</el-select>
</el-form-item>
</div>
<template #footer>
<el-button @click="batchAddDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBatchAdd">确定添加</el-button>
</template>
</el-dialog>
<!-- 编辑用户对话框 --> <!-- 编辑用户对话框 -->
<el-dialog v-model="editUserDialogVisible" title="编辑用户" width="500px"> <el-dialog v-model="editUserDialogVisible" title="编辑用户" width="500px">
...@@ -1054,7 +1025,7 @@ const previewImageData = ref({}) ...@@ -1054,7 +1025,7 @@ const previewImageData = ref({})
// 对话框显示状态 // 对话框显示状态
const addUserDialogVisible = ref(false) const addUserDialogVisible = ref(false)
const addInstitutionDialogVisible = ref(false) const addInstitutionDialogVisible = ref(false)
const batchAddDialogVisible = ref(false)
const editUserDialogVisible = ref(false) const editUserDialogVisible = ref(false)
const transferDialogVisible = ref(false) const transferDialogVisible = ref(false)
const userViewDialogVisible = ref(false) const userViewDialogVisible = ref(false)
...@@ -1112,9 +1083,7 @@ const addInstitutionForm = reactive({ ...@@ -1112,9 +1083,7 @@ const addInstitutionForm = reactive({
ownerId: null ownerId: null
}) })
// 批量添加
const batchAddText = ref('')
const batchAddOwnerId = ref(null)
// 编辑用户表单 // 编辑用户表单
const editUserForm = reactive({ const editUserForm = reactive({
...@@ -1695,89 +1664,7 @@ const submitAddInstitution = async () => { ...@@ -1695,89 +1664,7 @@ const submitAddInstitution = async () => {
} }
} }
/**
* 显示批量添加对话框
*/
const showBatchAddDialog = () => {
batchAddText.value = ''
batchAddOwnerId.value = null
batchAddDialogVisible.value = true
}
/**
* 提交批量添加
*/
const submitBatchAdd = () => {
const lines = batchAddText.value
.split('\n')
.map(line => line.trim())
.filter(line => line)
if (lines.length === 0) {
ElMessage.error('请输入机构信息!')
return
}
let addedCount = 0
let errorCount = 0
const errors = []
lines.forEach((line, index) => {
try {
// 使用空格分隔机构ID和机构名称
const parts = line.split(/\s+/).filter(part => part)
if (parts.length < 2) {
errors.push(`第${index + 1}行:格式错误,请使用"机构ID 机构名称"格式`)
errorCount++
return
}
const institutionId = parts[0]
const name = parts.slice(1).join(' ') // 支持机构名称包含空格
// 验证机构ID是否为数字
if (!/^\d+$/.test(institutionId)) {
errors.push(`第${index + 1}行:机构ID "${institutionId}" 必须为数字`)
errorCount++
return
}
if (!name) {
errors.push(`第${index + 1}行:机构名称不能为空`)
errorCount++
return
}
// 检查机构ID是否重复
if (dataStore.isInstitutionIdExists(institutionId)) {
errors.push(`第${index + 1}行:机构ID ${institutionId} 已存在`)
errorCount++
return
}
dataStore.addInstitution({
institutionId,
name,
ownerId: batchAddOwnerId.value
})
addedCount++
} catch (error) {
errors.push(`第${index + 1}行:${error.message}`)
errorCount++
}
})
if (errors.length > 0) {
ElMessage.warning(`成功添加 ${addedCount} 个机构,${errorCount} 个失败:\n${errors.slice(0, 3).join('\n')}${errors.length > 3 ? '\n...' : ''}`)
} else {
ElMessage.success(`成功添加 ${addedCount} 个机构!`)
}
if (addedCount > 0) {
batchAddDialogVisible.value = false
}
}
/** /**
* 编辑机构 * 编辑机构
...@@ -1853,25 +1740,45 @@ const handleInstitutionSelection = (selection) => { ...@@ -1853,25 +1740,45 @@ const handleInstitutionSelection = (selection) => {
/** /**
* 显示批量删除对话框 * 显示批量删除对话框
*/ */
const showBatchDeleteDialog = () => { const showBatchDeleteDialog = async () => {
if (selectedInstitutions.value.length === 0) { if (selectedInstitutions.value.length === 0) {
ElMessage.warning('请先选择要删除的机构!') ElMessage.warning('请先选择要删除的机构!')
return return
} }
ElMessageBox.confirm( try {
`确定要删除选中的 ${selectedInstitutions.value.length} 个机构吗?`, await ElMessageBox.confirm(
'批量删除', `确定要删除选中的 ${selectedInstitutions.value.length} 个机构吗?`,
{ type: 'warning' } '批量删除',
).then(() => { { type: 'warning' }
selectedInstitutions.value.forEach(inst => { )
dataStore.deleteInstitution(inst.id)
}) // 调用批量删除API
ElMessage.success('批量删除成功!') const institutionIds = selectedInstitutions.value.map(inst => inst.id)
const result = await dataStore.batchDeleteInstitutions(institutionIds)
// 显示结果
let message = `批量删除完成!\n成功删除:${result.success_count} 个机构`
if (result.error_count > 0) {
message += `\n失败:${result.error_count} 个`
if (result.errors.length > 0) {
const errorMsg = result.errors.slice(0, 3).join('\n') + (result.errors.length > 3 ? '\n...' : '')
message += `\n\n错误详情:\n${errorMsg}`
}
}
if (result.error_count > 0) {
ElMessage.warning(message)
} else {
ElMessage.success(message)
}
institutionTable.value.clearSelection() institutionTable.value.clearSelection()
}).catch(() => { } catch (error) {
// 用户取消 if (error !== 'cancel') {
}) ElMessage.error(`批量删除失败:${error.message}`)
}
}
} }
/** /**
...@@ -2418,7 +2325,7 @@ const handleExcelUpload = (file) => { ...@@ -2418,7 +2325,7 @@ const handleExcelUpload = (file) => {
*/ */
const processExcelFile = (file) => { const processExcelFile = (file) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e) => { reader.onload = async (e) => {
try { try {
const data = new Uint8Array(e.target.result) const data = new Uint8Array(e.target.result)
const workbook = window.XLSX.read(data, { type: 'array' }) const workbook = window.XLSX.read(data, { type: 'array' })
...@@ -2443,9 +2350,9 @@ const processExcelFile = (file) => { ...@@ -2443,9 +2350,9 @@ const processExcelFile = (file) => {
return return
} }
let addedCount = 0 // 准备批量添加的数据
let errorCount = 0 const institutionsToAdd = []
const errors = [] const validationErrors = []
dataRows.forEach((row, index) => { dataRows.forEach((row, index) => {
try { try {
...@@ -2455,28 +2362,24 @@ const processExcelFile = (file) => { ...@@ -2455,28 +2362,24 @@ const processExcelFile = (file) => {
// 验证必填字段 // 验证必填字段
if (!institutionId) { if (!institutionId) {
errors.push(`第${index + 2}行:机构ID不能为空`) validationErrors.push(`第${index + 2}行:机构ID不能为空`)
errorCount++
return return
} }
if (!name) { if (!name) {
errors.push(`第${index + 2}行:机构名称不能为空`) validationErrors.push(`第${index + 2}行:机构名称不能为空`)
errorCount++
return return
} }
// 验证机构ID是否为数字 // 验证机构ID是否为数字
if (!/^\d+$/.test(institutionId)) { if (!/^\d+$/.test(institutionId)) {
errors.push(`第${index + 2}行:机构ID "${institutionId}" 必须为数字`) validationErrors.push(`第${index + 2}行:机构ID "${institutionId}" 必须为数字`)
errorCount++
return return
} }
// 检查机构ID是否重复 // 检查机构ID是否重复(本地检查)
if (dataStore.isInstitutionIdExists(institutionId)) { if (dataStore.isInstitutionIdExists(institutionId)) {
errors.push(`第${index + 2}行:机构ID ${institutionId} 已存在`) validationErrors.push(`第${index + 2}行:机构ID ${institutionId} 已存在`)
errorCount++
return return
} }
...@@ -2487,30 +2390,59 @@ const processExcelFile = (file) => { ...@@ -2487,30 +2390,59 @@ const processExcelFile = (file) => {
if (owner) { if (owner) {
ownerId = owner.id ownerId = owner.id
} else { } else {
errors.push(`第${index + 2}行:找不到负责人 "${ownerName}",将设为公池机构`) validationErrors.push(`第${index + 2}行:找不到负责人 "${ownerName}",将设为公池机构`)
} }
} }
// 添加机构 // 添加到批量创建列表
dataStore.addInstitution({ institutionsToAdd.push({
institutionId, id: `inst_${Date.now()}_${Math.random().toString(36).substring(2, 11)}_${index}`,
name, institution_id: institutionId,
ownerId name: name,
owner_id: ownerId
}) })
addedCount++
} catch (error) { } catch (error) {
errors.push(`第${index + 2}行:${error.message}`) validationErrors.push(`第${index + 2}行:${error.message}`)
errorCount++
} }
}) })
// 显示结果 // 如果有有效数据,调用批量添加API
if (errors.length > 0) { if (institutionsToAdd.length > 0) {
const errorMsg = errors.slice(0, 5).join('\n') + (errors.length > 5 ? '\n...' : '') try {
ElMessage.warning(`Excel处理完成!\n成功添加:${addedCount} 个机构\n失败:${errorCount} 个\n\n错误详情:\n${errorMsg}`) const result = await dataStore.batchAddInstitutions(institutionsToAdd.map(inst => ({
id: inst.id,
institutionId: inst.institution_id,
name: inst.name,
ownerId: inst.owner_id
})))
// 显示结果
let message = `Excel处理完成!\n成功添加:${result.success_count} 个机构`
if (result.error_count > 0) {
message += `\n失败:${result.error_count} 个`
if (result.errors.length > 0) {
const errorMsg = result.errors.slice(0, 5).join('\n') + (result.errors.length > 5 ? '\n...' : '')
message += `\n\n错误详情:\n${errorMsg}`
}
}
if (validationErrors.length > 0) {
const validationMsg = validationErrors.slice(0, 3).join('\n') + (validationErrors.length > 3 ? '\n...' : '')
message += `\n\n验证错误:\n${validationMsg}`
}
if (result.error_count > 0 || validationErrors.length > 0) {
ElMessage.warning(message)
} else {
ElMessage.success(message)
}
} catch (error) {
ElMessage.error(`批量添加失败:${error.message}`)
}
} else { } else {
ElMessage.success(`Excel处理完成!成功添加 ${addedCount} 个机构`) // 只有验证错误,没有有效数据
const errorMsg = validationErrors.slice(0, 5).join('\n') + (validationErrors.length > 5 ? '\n...' : '')
ElMessage.error(`Excel文件中没有有效数据!\n\n错误详情:\n${errorMsg}`)
} }
} catch (error) { } catch (error) {
...@@ -2688,15 +2620,23 @@ const showMonthlyResetConfirm = async () => { ...@@ -2688,15 +2620,23 @@ const showMonthlyResetConfirm = async () => {
} }
/** /**
* 加载上次重置时间 * 加载上次重置时间(数据库模式)
*/ */
const loadLastResetTime = () => { const loadLastResetTime = async () => {
const lastReset = localStorage.getItem('last_reset_time') try {
if (lastReset) { // 从数据库获取系统配置中的重置时间
const date = new Date(lastReset) const config = await dataStore.systemConfig
lastResetTime.value = date.toLocaleString('zh-CN') const lastReset = config.last_reset_time
} else {
lastResetTime.value = '未重置' if (lastReset) {
const date = new Date(lastReset)
lastResetTime.value = date.toLocaleString('zh-CN')
} else {
lastResetTime.value = '未重置'
}
} catch (error) {
console.error('获取重置时间失败:', error)
lastResetTime.value = '未知'
} }
} }
...@@ -2737,7 +2677,7 @@ const saveCurrentMonthStats = async () => { ...@@ -2737,7 +2677,7 @@ const saveCurrentMonthStats = async () => {
) )
saveStatsLoading.value = true saveStatsLoading.value = true
const success = dataStore.saveCurrentMonthStats() const success = await dataStore.saveCurrentMonthStats()
if (success) { if (success) {
ElMessage.success('当前月份统计数据保存成功!') ElMessage.success('当前月份统计数据保存成功!')
...@@ -3100,8 +3040,7 @@ onMounted(() => { ...@@ -3100,8 +3040,7 @@ onMounted(() => {
// 加载上次重置时间 // 加载上次重置时间
loadLastResetTime() loadLastResetTime()
// 自动保存月度统计(如果需要) // 管理员面板初始化完成
dataStore.autoSaveMonthlyStats()
}) })
// 注册图标组件 // 注册图标组件
......
...@@ -104,13 +104,13 @@ const handleLogin = async () => { ...@@ -104,13 +104,13 @@ const handleLogin = async () => {
loading.value = true loading.value = true
// 执行登录 // 执行登录
const success = authStore.login(loginForm.phone, loginForm.password) const success = await authStore.login(loginForm.phone, loginForm.password)
if (success) { if (success) {
ElMessage.success('登录成功!') ElMessage.success('登录成功!')
// 根据用户角色跳转 // 根据用户角色跳转
if (authStore.currentUser.role === 'admin') { if (authStore.currentUser && authStore.currentUser.role === 'admin') {
router.push('/admin') router.push('/admin')
} else { } else {
router.push('/user') router.push('/user')
...@@ -130,8 +130,8 @@ const handleLogin = async () => { ...@@ -130,8 +130,8 @@ const handleLogin = async () => {
* 组件挂载时初始化数据 * 组件挂载时初始化数据
*/ */
onMounted(() => { onMounted(() => {
// 加载系统数据 // 登录页面不需要加载数据,数据将在登录成功后加载
dataStore.loadFromStorage() console.log('登录页面已加载')
}) })
</script> </script>
......
...@@ -294,20 +294,7 @@ const currentPeriod = computed(() => { ...@@ -294,20 +294,7 @@ const currentPeriod = computed(() => {
const year = currentDate.getFullYear() const year = currentDate.getFullYear()
const month = currentDate.getMonth() + 1 const month = currentDate.getMonth() + 1
// 获取上次重置时间 // 数据库模式下,直接显示当前年月
const lastResetTime = localStorage.getItem('last_reset_time')
if (lastResetTime) {
const resetDate = new Date(lastResetTime)
const resetMonth = resetDate.getMonth() + 1
const resetYear = resetDate.getFullYear()
// 如果是同一年同一月,显示重置后的天数
if (resetYear === year && resetMonth === month) {
const daysSinceReset = Math.floor((currentDate - resetDate) / (1000 * 60 * 60 * 24))
return `${year}${month}月(重置后第${daysSinceReset + 1}天)`
}
}
return `${year}${month}月` return `${year}${month}月`
}) })
...@@ -400,14 +387,7 @@ const beforeUpload = (file, institutionId) => { ...@@ -400,14 +387,7 @@ const beforeUpload = (file, institutionId) => {
return false return false
} }
// 🔍 使用新的重复检测功能(暂时创建临时图片数据进行检测) // 基础文件验证
const tempImageData = {
name: file.name,
size: file.size,
url: 'temp_url_for_detection' // 临时URL,实际检测会在压缩后进行
}
// 注意:这里只是基础检测,完整的重复检测会在实际上传时进行
const isImage = file.type.startsWith('image/') const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5 const isLt5M = file.size / 1024 / 1024 < 5
...@@ -460,18 +440,8 @@ const handleImageUpload = (uploadFile, institutionId) => { ...@@ -460,18 +440,8 @@ const handleImageUpload = (uploadFile, institutionId) => {
currentImageCount: institution.images.length currentImageCount: institution.images.length
}) })
// 检查localStorage中的实际数据 // 数据库模式下,数据直接从内存状态获取
const savedData = JSON.parse(localStorage.getItem('score_system_institutions') || '[]') console.log('数据库模式:机构数据来自 API')
const savedInstitution = savedData.find(inst => inst.id === institutionId)
console.log('localStorage中的机构数据:', {
institutionId,
savedInstitution: savedInstitution ? {
id: savedInstitution.id,
institutionId: savedInstitution.institutionId,
name: savedInstitution.name,
imagesCount: savedInstitution.images?.length || 0
} : null
})
// 验证机构ID匹配 // 验证机构ID匹配
if (savedInstitution && savedInstitution.id !== institutionId) { if (savedInstitution && savedInstitution.id !== institutionId) {
...@@ -614,19 +584,14 @@ const removeImage = async (institutionId, imageId) => { ...@@ -614,19 +584,14 @@ const removeImage = async (institutionId, imageId) => {
ElMessage.success('图片删除成功!') ElMessage.success('图片删除成功!')
// 强制刷新数据确保界面更新 // 强制刷新数据确保界面更新
nextTick(() => { nextTick(async () => {
// 重新加载数据以确保界面同步 // 重新加载数据以确保界面同步
dataStore.loadFromStorage() await dataStore.loadData()
// 验证删除结果 // 验证删除结果
const userInstitutions = dataStore.getInstitutionsByUserId(authStore.currentUser.id) const userInstitutions = dataStore.getInstitutionsByUserId(authStore.currentUser.id)
const institution = userInstitutions.find(inst => inst.id === institutionId) const institution = userInstitutions.find(inst => inst.id === institutionId)
console.log('删除后机构图片数量:', institution?.images.length || 0) 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) { } catch (error) {
...@@ -729,16 +694,8 @@ onMounted(() => { ...@@ -729,16 +694,8 @@ onMounted(() => {
// 调试:检查页面加载时的数据状态 // 用户面板初始化完成
console.log('=== 页面加载时数据状态 ===') console.log('用户面板已加载,用户:', authStore.currentUser?.name)
console.log('当前用户:', authStore.currentUser)
console.log('用户机构数量:', userInstitutions.value.length)
console.log('localStorage机构数据:', localStorage.getItem('score_system_institutions'))
// 检查每个机构的图片数量
userInstitutions.value.forEach(inst => {
console.log(`机构 ${inst.name} 图片数量:`, inst.images.length)
})
}) })
......
@echo off
chcp 65001 >nul
echo ========================================
echo 绩效计分系统 - 数据库启动脚本
echo ========================================
echo.
REM 检查 Docker 是否安装
where docker >nul 2>&1
if errorlevel 1 (
echo ❌ 未检测到 Docker,请先安装 Docker Desktop
pause
exit /b 1
)
REM 检查 Docker 是否运行
docker info >nul 2>&1
if errorlevel 1 (
echo ❌ Docker 未运行,请启动 Docker Desktop
pause
exit /b 1
)
echo ✅ Docker 环境检查通过
echo.
echo 🚀 启动 PostgreSQL 数据库...
docker compose up -d postgres
if errorlevel 1 (
echo ❌ 数据库启动失败
pause
exit /b 1
)
echo.
echo ⏳ 等待数据库初始化完成...
timeout /t 10 /nobreak >nul
REM 检查数据库健康状态
echo 🔍 检查数据库连接状态...
docker compose exec postgres pg_isready -U performance_user -d performance_db >nul 2>&1
if errorlevel 1 (
echo ⚠️ 数据库可能还在初始化中,请稍后检查
) else (
echo ✅ 数据库连接正常
)
echo.
echo ========================================
echo 🎉 数据库启动完成!
echo ========================================
echo.
echo 📋 数据库信息:
echo - 主机: localhost
echo - 端口: 5432
echo - 数据库: performance_db
echo - 用户: performance_user
echo - 密码: performance_pass
echo.
echo 🔧 管理命令:
echo - 查看日志: docker compose logs postgres
echo - 停止数据库: docker compose stop postgres
echo - 连接数据库: docker compose exec postgres psql -U performance_user -d performance_db
echo.
pause
@echo off
chcp 65001 >nul
echo ========================================
echo 绩效计分系统 - 完整启动脚本
echo ========================================
echo.
REM 检查 Docker 是否安装
where docker >nul 2>&1
if errorlevel 1 (
echo ❌ 未检测到 Docker,请先安装 Docker Desktop
pause
exit /b 1
)
REM 检查 Docker 是否运行
docker info >nul 2>&1
if errorlevel 1 (
echo ❌ Docker 未运行,请启动 Docker Desktop
pause
exit /b 1
)
echo ✅ Docker 环境检查通过
echo.
echo 🚀 启动完整系统...
echo.
echo 📊 1. 启动 PostgreSQL 数据库...
docker compose up -d postgres
if errorlevel 1 (
echo ❌ 数据库启动失败
pause
exit /b 1
)
echo ⏳ 等待数据库初始化完成...
timeout /t 15 /nobreak >nul
echo 🔍 检查数据库连接状态...
docker compose exec postgres pg_isready -U performance_user -d performance_db >nul 2>&1
if errorlevel 1 (
echo ⚠️ 数据库可能还在初始化中,继续启动其他服务...
) else (
echo ✅ 数据库连接正常
)
echo.
echo 🔧 2. 启动 FastAPI 后端服务...
docker compose up -d api
if errorlevel 1 (
echo ❌ 后端服务启动失败
pause
exit /b 1
)
echo ⏳ 等待后端服务启动...
timeout /t 10 /nobreak >nul
echo.
echo 🌐 3. 启动前端应用...
docker compose up -d frontend
if errorlevel 1 (
echo ❌ 前端应用启动失败
pause
exit /b 1
)
echo.
echo ========================================
echo 🎉 系统启动完成!
echo ========================================
echo.
echo 📋 服务信息:
echo - 前端应用: http://localhost:4001
echo - 后端API: http://localhost:8000
echo - API文档: http://localhost:8000/docs
echo - 数据库: localhost:5432
echo.
echo 🔧 管理命令:
echo - 查看日志: docker compose logs -f
echo - 停止系统: docker compose down
echo - 重启系统: docker compose restart
echo.
echo 📖 默认登录信息:
echo - 用户名: admin
echo - 密码: admin123
echo.
REM 等待5秒后自动打开浏览器
echo 🌐 5秒后自动打开浏览器...
timeout /t 5 /nobreak >nul
start http://localhost:4001
pause
# 测试批量删除API
$loginData = @{phone='admin'; password='admin123'} | ConvertTo-Json
$loginResponse = Invoke-RestMethod -Uri 'http://localhost:8000/api/users/login' -Method Post -Body $loginData -ContentType 'application/json'
Write-Host 'Login successful, token obtained'
$token = $loginResponse.access_token
$headers = @{Authorization="Bearer $token"; 'Content-Type'='application/json'}
$deleteData = @{institution_ids=@('test_id')} | ConvertTo-Json
Write-Host 'Testing batch delete API...'
try {
$deleteResponse = Invoke-RestMethod -Uri 'http://localhost:8000/api/institutions/batch' -Method Delete -Body $deleteData -Headers $headers
Write-Host 'Success:' ($deleteResponse | ConvertTo-Json)
} catch {
Write-Host 'Error Status:' $_.Exception.Response.StatusCode
Write-Host 'Error Message:' $_.Exception.Message
}
#!/usr/bin/env python3
"""测试批量删除API"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'backend'))
import requests
import json
def test_batch_delete():
base_url = "http://localhost:8000"
# 1. 登录获取token
print("1. 登录...")
login_data = {
"username": "admin",
"password": "admin123"
}
try:
login_response = requests.post(f"{base_url}/api/users/login", json=login_data)
print(f"登录响应状态码: {login_response.status_code}")
if login_response.status_code != 200:
print(f"登录失败: {login_response.text}")
return
token = login_response.json()["access_token"]
print("登录成功,获取到token")
# 2. 设置请求头
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 3. 获取机构列表
print("\n2. 获取机构列表...")
institutions_response = requests.get(f"{base_url}/api/institutions/", headers=headers)
print(f"获取机构列表响应状态码: {institutions_response.status_code}")
if institutions_response.status_code == 200:
institutions = institutions_response.json()
print(f"找到 {len(institutions)} 个机构")
if institutions:
first_institution = institutions[0]
print(f"第一个机构: ID={first_institution.get('id')}, 名称={first_institution.get('name')}")
# 4. 测试批量删除API
print("\n3. 测试批量删除API...")
delete_data = {
"institution_ids": ["test_id_that_does_not_exist"]
}
delete_response = requests.delete(
f"{base_url}/api/institutions/batch",
json=delete_data,
headers=headers
)
print(f"批量删除响应状态码: {delete_response.status_code}")
print(f"批量删除响应内容: {delete_response.text}")
except requests.exceptions.RequestException as e:
print(f"请求异常: {e}")
except Exception as e:
print(f"其他异常: {e}")
if __name__ == "__main__":
test_batch_delete()
机构ID,机构名称,负责人
888,测试批量添加机构1,陈锐屏
999,测试批量添加机构2,张田田
# 测试批量删除路由
Write-Host 'Testing batch route...'
try {
$testResponse = Invoke-RestMethod -Uri 'http://localhost:8000/api/institutions/batch/test' -Method Get
Write-Host 'Test route success:' ($testResponse | ConvertTo-Json)
} catch {
Write-Host 'Test route error:' $_.Exception.Message
Write-Host 'Status code:' $_.Exception.Response.StatusCode
}
...@@ -10,9 +10,22 @@ export default defineConfig({ ...@@ -10,9 +10,22 @@ export default defineConfig({
} }
}, },
server: { server: {
port: 4001, port: 5173,
host: '0.0.0.0', host: '0.0.0.0',
hmr: true hmr: true,
proxy: {
// 代理 API 请求到后端服务
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false
},
'/health': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false
}
}
}, },
build: { build: {
rollupOptions: { rollupOptions: {
...@@ -20,5 +33,9 @@ export default defineConfig({ ...@@ -20,5 +33,9 @@ export default defineConfig({
manualChunks: undefined manualChunks: undefined
} }
} }
},
// 环境变量配置
define: {
__VUE_PROD_DEVTOOLS__: false
} }
}) })
\ No newline at end of file
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