Commit eb34b9e8 by luoqi

fix:机构删除

parent ccdb2e26
# 前端环境变量配置
# API 服务地址
VITE_API_BASE_URL=http://localhost:8000
# 应用配置
VITE_APP_TITLE=绩效计分系统
VITE_APP_VERSION=8.8.0
# 开发模式配置
VITE_DEV_MODE=true
...@@ -9,7 +9,6 @@ dist/ ...@@ -9,7 +9,6 @@ dist/
build/ build/
# Environment variables # Environment variables
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
...@@ -95,3 +94,30 @@ jspm_packages/ ...@@ -95,3 +94,30 @@ jspm_packages/
# TernJS port file # TernJS port file
.tern-port .tern-port
node_modules node_modules
.history
# Python cache files
__pycache__/
*.py[cod]
*$py.class
# Backup files
*.backup
*_backup*
*_old*
*_new*
*.tmp
*.temp
# Test files
test_*.py
test_*.js
test_*.csv
test.ps1
test_*.ps1
# SQLite databases (using PostgreSQL now)
*.db
*.sqlite
*.sqlite3
# 数据库配置
DATABASE_URL=postgresql://performance_user:performance_pass@localhost:5432/performance_db
# API 服务配置
API_HOST=0.0.0.0
API_PORT=8000
# CORS 配置(JSON 数组格式)
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
...@@ -5,8 +5,8 @@ DATABASE_URL=postgresql://performance_user:performance_pass@localhost:5432/perfo ...@@ -5,8 +5,8 @@ DATABASE_URL=postgresql://performance_user:performance_pass@localhost:5432/perfo
API_HOST=0.0.0.0 API_HOST=0.0.0.0
API_PORT=8000 API_PORT=8000
# CORS 配置(多个域名用逗号分隔 # CORS 配置(JSON 数组格式
CORS_ORIGINS=http://localhost:5173,http://localhost:8080,http://localhost:4001 CORS_ORIGINS=["http://localhost:5173","http://localhost:8080","http://localhost:4001"]
# 安全配置 # 安全配置
SECRET_KEY=your-secret-key-here-change-in-production SECRET_KEY=your-secret-key-here-change-in-production
......
...@@ -429,7 +429,7 @@ async def test_batch_route(): ...@@ -429,7 +429,7 @@ async def test_batch_route():
return {"message": "批量删除路由正常工作"} return {"message": "批量删除路由正常工作"}
@router.delete("/batch-delete", response_model=InstitutionBatchResponse, summary="批量删除机构") @router.post("/batch-delete", response_model=InstitutionBatchResponse, summary="批量删除机构")
async def batch_delete_institutions( async def batch_delete_institutions(
batch_data: InstitutionBatchDelete, batch_data: InstitutionBatchDelete,
db: DatabaseManager = Depends(get_database), db: DatabaseManager = Depends(get_database),
......
"""
机构管理 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="获取机构失败")
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 8080;
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 处理CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
let filePath = req.url === '/' ? '/index.html' : req.url;
// 移除查询参数
filePath = filePath.split('?')[0];
// 安全检查,防止目录遍历
if (filePath.includes('..')) {
res.writeHead(400);
res.end('Bad Request');
return;
}
// 确定文件的完整路径
let fullPath;
if (filePath.startsWith('/public/')) {
fullPath = path.join(__dirname, filePath);
} else if (filePath.startsWith('/assets/')) {
fullPath = path.join(__dirname, 'dist', filePath);
} else if (filePath === '/index.html') {
fullPath = path.join(__dirname, 'dist', 'index.html');
} else {
// 尝试在dist目录中查找
fullPath = path.join(__dirname, 'dist', filePath);
if (!fs.existsSync(fullPath)) {
// 如果在dist中找不到,尝试public目录
fullPath = path.join(__dirname, 'public', filePath);
if (!fs.existsSync(fullPath)) {
// 最后尝试根目录
fullPath = path.join(__dirname, filePath.substring(1));
}
}
}
// 检查文件是否存在
fs.access(fullPath, fs.constants.F_OK, (err) => {
if (err) {
console.log(`文件不存在: ${fullPath}`);
res.writeHead(404);
res.end('File Not Found');
return;
}
// 获取文件扩展名
const ext = path.extname(fullPath).toLowerCase();
const mimeType = mimeTypes[ext] || 'application/octet-stream';
// 读取并发送文件
fs.readFile(fullPath, (err, data) => {
if (err) {
console.error(`读取文件错误: ${err}`);
res.writeHead(500);
res.end('Internal Server Error');
return;
}
res.writeHead(200, { 'Content-Type': mimeType });
res.end(data);
});
});
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`🚀 绩效计分系统 v8.6 服务器已启动`);
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主系统: http://localhost:${PORT}/index.html`);
console.log(`🔍 诊断页面: http://localhost:${PORT}/public/diagnose.html`);
console.log('');
console.log('🎉 v8.6版本已启动,现在可以体验新功能!');
console.log('');
console.log('按 Ctrl+C 停止服务器');
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ 端口 ${PORT} 已被占用,请关闭其他服务或使用其他端口`);
} else {
console.error(`❌ 服务器错误: ${err}`);
}
});
version: '3.8'
services: services:
# PostgreSQL 数据库服务 # PostgreSQL 数据库服务
postgres: postgres:
......
-- 绩效计分系统数据库初始化脚本
-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100),
full_name VARCHAR(100),
role VARCHAR(20) DEFAULT 'user',
department VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
-- 创建工作台表
CREATE TABLE IF NOT EXISTS workstations (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
location VARCHAR(100),
department VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
-- 创建绩效记录表
CREATE TABLE IF NOT EXISTS performance_records (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
workstation_id INTEGER REFERENCES workstations(id),
score DECIMAL(5,2) NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
comments TEXT,
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建实时同步会话表
CREATE TABLE IF NOT EXISTS sync_sessions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
session_id VARCHAR(255) UNIQUE NOT NULL,
browser_info JSONB,
ip_address INET,
connected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
-- 创建数据变更日志表
CREATE TABLE IF NOT EXISTS change_logs (
id SERIAL PRIMARY KEY,
table_name VARCHAR(50) NOT NULL,
record_id INTEGER NOT NULL,
action VARCHAR(20) NOT NULL, -- INSERT, UPDATE, DELETE
old_data JSONB,
new_data JSONB,
user_id INTEGER REFERENCES users(id),
session_id VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建系统配置表
CREATE TABLE IF NOT EXISTS system_config (
id SERIAL PRIMARY KEY,
config_key VARCHAR(100) UNIQUE NOT NULL,
config_value TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入默认用户
INSERT INTO users (username, password_hash, email, full_name, role, department) VALUES
('admin', '$2b$10$rOzJqQjQjQjQjQjQjQjQjOzJqQjQjQjQjQjQjQjQjQjQjQjQjQjQj', 'admin@performance.local', '系统管理员', 'admin', 'IT部门'),
('user1', '$2b$10$rOzJqQjQjQjQjQjQjQjQjOzJqQjQjQjQjQjQjQjQjQjQjQjQjQjQj', 'user1@performance.local', '张三', 'user', '生产部门'),
('user2', '$2b$10$rOzJqQjQjQjQjQjQjQjQjOzJqQjQjQjQjQjQjQjQjQjQjQjQjQjQj', 'user2@performance.local', '李四', 'user', '质检部门')
ON CONFLICT (username) DO NOTHING;
-- 插入默认工作台
INSERT INTO workstations (name, description, location, department) VALUES
('昆明市五华区爱牙口腔诊所', '口腔医疗服务', '昆明市五华区', '医疗部门'),
('五华区长青口腔诊疗所', '口腔诊疗服务', '昆明市五华区', '医疗部门'),
('昆明美奥云口腔医院有限公司安宁宁湖诊所', '口腔专科医院', '昆明市安宁市', '医疗部门')
ON CONFLICT DO NOTHING;
-- 插入系统配置
INSERT INTO system_config (config_key, config_value, description) VALUES
('system_name', '绩效计分系统', '系统名称'),
('version', '8.4', '系统版本'),
('max_sessions_per_user', '5', '每个用户最大同时会话数'),
('sync_interval', '1000', '数据同步间隔(毫秒)'),
('session_timeout', '3600', '会话超时时间(秒)')
ON CONFLICT (config_key) DO UPDATE SET
config_value = EXCLUDED.config_value,
updated_at = CURRENT_TIMESTAMP;
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_performance_records_user_id ON performance_records(user_id);
CREATE INDEX IF NOT EXISTS idx_performance_records_workstation_id ON performance_records(workstation_id);
CREATE INDEX IF NOT EXISTS idx_performance_records_period ON performance_records(period_start, period_end);
CREATE INDEX IF NOT EXISTS idx_sync_sessions_user_id ON sync_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sync_sessions_session_id ON sync_sessions(session_id);
CREATE INDEX IF NOT EXISTS idx_change_logs_table_record ON change_logs(table_name, record_id);
CREATE INDEX IF NOT EXISTS idx_change_logs_created_at ON change_logs(created_at);
-- 创建触发器函数用于更新 updated_at 字段
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_workstations_updated_at BEFORE UPDATE ON workstations
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_performance_records_updated_at BEFORE UPDATE ON performance_records
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();
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 8080;
const DIST_DIR = path.join(__dirname, 'dist');
// MIME types
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm'
};
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
// Parse URL
let filePath = path.join(DIST_DIR, req.url === '/' ? 'index.html' : req.url);
// Security check
if (!filePath.startsWith(DIST_DIR)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
// Check if file exists
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
// If file doesn't exist and it's not a static asset, serve index.html (SPA routing)
if (!path.extname(filePath)) {
filePath = path.join(DIST_DIR, 'index.html');
} else {
res.writeHead(404);
res.end('Not Found');
return;
}
}
// Get file extension and MIME type
const extname = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[extname] || 'application/octet-stream';
// Read and serve file
fs.readFile(filePath, (err, content) => {
if (err) {
res.writeHead(500);
res.end('Server Error');
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
});
});
});
server.listen(PORT, 'localhost', () => {
console.log(`\n🚀 Server running at http://localhost:${PORT}`);
console.log(`📁 Serving files from: ${DIST_DIR}`);
console.log(`\n✅ 绩效计分系统已启动!`);
console.log(`🌐 请在浏览器中访问: http://localhost:${PORT}`);
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`❌ 端口 ${PORT} 已被占用,请尝试其他端口`);
} else {
console.log('❌ 服务器启动失败:', err);
}
});
...@@ -357,7 +357,7 @@ export const institutionApi = { ...@@ -357,7 +357,7 @@ export const institutionApi = {
// 批量删除机构 // 批量删除机构
async batchDelete(institutionIds) { async batchDelete(institutionIds) {
return apiClient.delete('/api/institutions/batch-delete', { institution_ids: institutionIds }) return apiClient.post('/api/institutions/batch-delete', { institution_ids: institutionIds })
} }
} }
......
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm',
'.vue': 'text/plain',
'.md': 'text/markdown'
};
const server = http.createServer((req, res) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// 解析URL
const parsedUrl = url.parse(req.url);
let pathname = parsedUrl.pathname;
// 如果是根路径,重定向到index.html
if (pathname === '/') {
pathname = '/index.html';
}
// 构建文件路径
const filePath = path.join(__dirname, pathname);
// 检查文件是否存在
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
// 文件不存在
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<head>
<title>404 - 文件未找到</title>
<meta charset="utf-8">
</head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1 style="color: #f56c6c;">404 - 文件未找到</h1>
<p>请求的文件 ${pathname} 不存在</p>
<p><a href="/" style="color: #409eff;">返回首页</a></p>
</body>
</html>
`);
return;
}
// 获取文件扩展名
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
// 读取文件
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<head>
<title>500 - 服务器错误</title>
<meta charset="utf-8">
</head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1 style="color: #f56c6c;">500 - 服务器错误</h1>
<p>读取文件时发生错误: ${err.message}</p>
</body>
</html>
`);
return;
}
// 设置CORS头部
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 设置缓存控制
if (ext === '.js' || ext === '.css') {
res.setHeader('Cache-Control', 'no-cache');
}
// 返回文件内容
res.writeHead(200, { 'Content-Type': contentType + '; charset=utf-8' });
res.end(data);
});
});
});
// 尝试不同的端口
const ports = [4001, 5174, 5175, 5176, 8080, 3000];
let currentPortIndex = 0;
function tryStartServer() {
const PORT = ports[currentPortIndex];
const HOST = '127.0.0.1';
const serverInstance = server.listen(PORT, HOST, () => {
console.log('========================================');
console.log('🚀 绩效计分系统 v8.6 优化版本服务器启动成功!');
console.log('========================================');
console.log(`📍 服务器地址: http://localhost:${PORT}`);
console.log(`🏠 主页面: http://localhost:${PORT}/index.html`);
console.log(`🧪 测试脚本: http://localhost:${PORT}/test-v86-optimizations.js`);
console.log(`📋 版本说明: http://localhost:${PORT}/v8.6优化版本说明.md`);
console.log('');
console.log('✨ v8.6 优化功能已启用:');
console.log(' • 智能图片压缩 (减少50-70%存储)');
console.log(' • 实时存储监控');
console.log(' • 自动备份恢复');
console.log(' • 增强用户体验');
console.log(' • 数据分离优化');
console.log('');
console.log('🔧 使用说明:');
console.log(' 1. 在浏览器中打开上述地址');
console.log(' 2. 使用 admin/admin123 登录管理员账户');
console.log(' 3. 查看"数据统计"标签的数据分离状态');
console.log(' 4. 测试"数据管理"标签的备份功能');
console.log(' 5. 体验优化后的图片上传功能');
console.log('');
console.log('按 Ctrl+C 停止服务器');
console.log('========================================');
// 自动打开浏览器
const { exec } = require('child_process');
exec(`start "" "http://localhost:${PORT}/index.html"`, (error) => {
if (error) {
console.log('💡 请手动在浏览器中打开: http://localhost:' + PORT);
}
});
});
serverInstance.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`⚠️ 端口 ${PORT} 被占用,尝试下一个端口...`);
currentPortIndex++;
if (currentPortIndex < ports.length) {
tryStartServer();
} else {
console.log('❌ 所有端口都被占用,请手动指定端口或关闭占用端口的程序');
process.exit(1);
}
} else {
console.error('❌ 服务器启动失败:', err.message);
process.exit(1);
}
});
}
// 启动服务器
tryStartServer();
// 优雅关闭
process.on('SIGINT', () => {
console.log('\n🛑 正在关闭服务器...');
server.close(() => {
console.log('✅ 服务器已关闭');
process.exit(0);
});
});
process.on('SIGTERM', () => {
console.log('\n🛑 收到终止信号,正在关闭服务器...');
server.close(() => {
console.log('✅ 服务器已关闭');
process.exit(0);
});
});
# 测试批量删除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
}
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