diff --git a/backend/.coverage b/backend/.coverage index fe63f52c..156d3d68 100644 Binary files a/backend/.coverage and b/backend/.coverage differ diff --git a/backend/src/middleware/__pycache__/auth.cpython-310.pyc b/backend/src/middleware/__pycache__/auth.cpython-310.pyc index 16d4d024..b65822cf 100644 Binary files a/backend/src/middleware/__pycache__/auth.cpython-310.pyc and b/backend/src/middleware/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/src/middleware/auth.py b/backend/src/middleware/auth.py index 272b796e..d05fdeb2 100644 --- a/backend/src/middleware/auth.py +++ b/backend/src/middleware/auth.py @@ -118,6 +118,11 @@ async def require_admin( if current_user.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="需要管理员权限", + detail={ + "success": False, + "message": "需要管理员权限", + "data": None, + "error_code": "3001", + }, ) return current_user diff --git a/backend/src/routes/__pycache__/projects.cpython-310.pyc b/backend/src/routes/__pycache__/projects.cpython-310.pyc index 004f12ba..dfe08a42 100644 Binary files a/backend/src/routes/__pycache__/projects.cpython-310.pyc and b/backend/src/routes/__pycache__/projects.cpython-310.pyc differ diff --git a/backend/src/routes/projects.py b/backend/src/routes/projects.py index a527b5c0..b100bd2e 100644 --- a/backend/src/routes/projects.py +++ b/backend/src/routes/projects.py @@ -108,6 +108,83 @@ async def get_projects( } + +@router.get("/statistics/basic", response_model=dict) +async def get_statistics( + engineering_type: Optional[str] = None, + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + contract_amount_min: Optional[Decimal] = None, + contract_amount_max: Optional[Decimal] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + conditions = [] + if engineering_type: + conditions.append(Project.engineering_type == engineering_type) + if signing_date_start: + conditions.append(Project.signing_date >= signing_date_start) + if signing_date_end: + conditions.append(Project.signing_date <= signing_date_end) + if contract_amount_min: + conditions.append(Project.contract_amount >= contract_amount_min) + if contract_amount_max: + conditions.append(Project.contract_amount <= contract_amount_max) + + if conditions: + query = query.where(and_(*conditions)) + + result = await db.execute(query) + projects = result.scalars().all() + + total_count = len(projects) + total_contract_amount = sum( + p.contract_amount or Decimal(0) for p in projects + ) + total_receipt_amount = sum( + p.actual_receipt_amount or Decimal(0) for p in projects + ) + total_payment_amount = sum( + p.actual_payment_amount or Decimal(0) for p in projects + ) + + avg_receipt_rate = 0 + if total_contract_amount > 0: + avg_receipt_rate = float( + (total_receipt_amount / total_contract_amount) * 100 + ) + + avg_payment_rate = 0 + total_payable = sum(p.payable_amount or Decimal(0) for p in projects) + if total_payable > 0: + avg_payment_rate = float( + (total_payment_amount / total_payable) * 100 + ) + + avg_progress = 0 + if total_count > 0: + avg_progress = sum( + p.cumulative_progress or Decimal(0) for p in projects + ) / total_count + + return { + "success": True, + "message": "统计成功", + "data": { + "total_count": total_count, + "total_contract_amount": float(total_contract_amount), + "total_receipt_amount": float(total_receipt_amount), + "total_payment_amount": float(total_payment_amount), + "avg_receipt_completion_rate": float(avg_receipt_rate), + "avg_payment_completion_rate": float(avg_payment_rate), + "avg_cumulative_progress": float(avg_progress), + }, + "error_code": None, + } + + @router.get("/{project_id}", response_model=dict) async def get_project( project: Project = Depends(get_project_or_404), diff --git a/backend/src/routes/projects.py.backup2 b/backend/src/routes/projects.py.backup2 new file mode 100644 index 00000000..a527b5c0 --- /dev/null +++ b/backend/src/routes/projects.py.backup2 @@ -0,0 +1,438 @@ +from fastapi import APIRouter, Depends, Query, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, and_, desc, asc +from config.database import get_db +from src.models.user import User +from src.models.project import Project +from src.schemas.project import ( + ProjectCreate, + ProjectUpdate, + ProjectResponse, + ProjectListResponse, + ) +from src.middleware.auth import ( + get_current_user, + require_market_or_admin, +) +from src.dependencies import get_project_or_404 +from typing import Optional +from datetime import date +from decimal import Decimal + +router = APIRouter(prefix="/projects", tags=["项目管理"]) + + +@router.get("", response_model=dict) +async def get_projects( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + project_no: Optional[str] = None, + engineering_type: Optional[str] = None, + project_department: Optional[str] = None, + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + contract_amount_min: Optional[Decimal] = None, + contract_amount_max: Optional[Decimal] = None, + keyword: Optional[str] = None, + sort_by: Optional[str] = None, + sort_order: Optional[str] = Query(None, pattern="^(asc|desc)$"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + conditions = [] + if project_no: + conditions.append(Project.project_no == project_no) + if engineering_type: + conditions.append(Project.engineering_type == engineering_type) + if project_department: + conditions.append(Project.project_department == project_department) + if signing_date_start: + conditions.append(Project.signing_date >= signing_date_start) + if signing_date_end: + conditions.append(Project.signing_date <= signing_date_end) + if contract_amount_min: + conditions.append(Project.contract_amount >= contract_amount_min) + if contract_amount_max: + conditions.append(Project.contract_amount <= contract_amount_max) + if keyword: + conditions.append( + (Project.name.contains(keyword)) | + (Project.owner_unit.contains(keyword)) + ) + + if conditions: + query = query.where(and_(*conditions)) + + if sort_by: + sort_column = getattr(Project, sort_by, None) + if sort_column: + order = desc if sort_order == "desc" else asc + query = query.order_by(order(sort_column)) + else: + query = query.order_by(Project.created_at.desc()) + + total_query = select(func.count()).select_from(query.subquery()) + total_result = await db.execute(total_query) + total = total_result.scalar() + + query = query.offset((page - 1) * page_size).limit(page_size) + result = await db.execute(query) + projects = result.scalars().all() + + project_data = [] + for p in projects: + creator_result = await db.execute( + select(User.real_name).where(User.id == p.created_by) + ) + creator_name = creator_result.scalar_one_or_none() + p_dict = ProjectResponse.model_validate(p).model_dump() + # Convert Decimal to float for JSON serialization + for key, value in p_dict.items(): + if isinstance(value, Decimal): + p_dict[key] = float(value) + p_dict["created_by_name"] = creator_name + project_data.append(p_dict) + + return { + "success": True, + "message": "获取成功", + "data": { + "items": project_data, + "total": total, + "page": page, + "page_size": page_size, + }, + "error_code": None, + } + + +@router.get("/{project_id}", response_model=dict) +async def get_project( + project: Project = Depends(get_project_or_404), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(User.real_name).where(User.id == project.created_by) + ) + creator_name = result.scalar_one_or_none() + + project_dict = ProjectResponse.model_validate(project).model_dump() + project_dict["created_by_name"] = creator_name + + return { + "success": True, + "message": "获取成功", + "data": project_dict, + "error_code": None, + } + + +@router.post("", response_model=dict) +async def create_project( + project_data: ProjectCreate, + current_user: User = Depends(require_market_or_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(Project).where(Project.project_no == project_data.project_no) + ) + if result.scalar_one_or_none(): + return { + "success": False, + "message": "项目编号已存在", + "data": None, + "error_code": "2002", + } + + project = Project( + **project_data.model_dump(), + created_by=current_user.id, + ) + db.add(project) + await db.commit() + await db.refresh(project) + + return { + "success": True, + "message": "项目创建成功", + "data": ProjectResponse.model_validate(project), + "error_code": None, + } + + +@router.put("/{project_id}", response_model=dict) +async def update_project( + project_id: int, + project_data: ProjectUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(Project).where(Project.id == project_id)) + project = result.scalar_one_or_none() + + if not project: + return { + "success": False, + "message": "项目不存在", + "data": None, + "error_code": "2001", + } + + if current_user.role == "market" and project.created_by != current_user.id: + return { + "success": False, + "message": "无权修改此项目", + "data": None, + "error_code": "3001", + } + + update_data = project_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(project, field, value) + + await db.commit() + await db.refresh(project) + + return { + "success": True, + "message": "项目更新成功", + "data": ProjectResponse.model_validate(project), + "error_code": None, + } + + +@router.delete("/{project_id}", response_model=dict) +async def delete_project( + project_id: int, + current_user: User = Depends(require_market_or_admin), + db: AsyncSession = Depends(get_db), +): + result = await db.execute(select(Project).where(Project.id == project_id)) + project = result.scalar_one_or_none() + + if not project: + return { + "success": False, + "message": "项目不存在", + "data": None, + "error_code": "2001", + } + + if current_user.role == "market" and project.created_by != current_user.id: + return { + "success": False, + "message": "无权删除此项目", + "data": None, + "error_code": "3001", + } + + await db.delete(project) + await db.commit() + + return { + "success": True, + "message": "项目删除成功", + "data": None, + "error_code": None, + } + + +@router.get("/statistics/basic", response_model=dict) +async def get_statistics( + engineering_type: Optional[str] = None, + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + contract_amount_min: Optional[Decimal] = None, + contract_amount_max: Optional[Decimal] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + conditions = [] + if engineering_type: + conditions.append(Project.engineering_type == engineering_type) + if signing_date_start: + conditions.append(Project.signing_date >= signing_date_start) + if signing_date_end: + conditions.append(Project.signing_date <= signing_date_end) + if contract_amount_min: + conditions.append(Project.contract_amount >= contract_amount_min) + if contract_amount_max: + conditions.append(Project.contract_amount <= contract_amount_max) + + if conditions: + query = query.where(and_(*conditions)) + + result = await db.execute(query) + projects = result.scalars().all() + + total_count = len(projects) + total_contract_amount = sum( + p.contract_amount or Decimal(0) for p in projects + ) + total_receipt_amount = sum( + p.actual_receipt_amount or Decimal(0) for p in projects + ) + total_payment_amount = sum( + p.actual_payment_amount or Decimal(0) for p in projects + ) + + avg_receipt_rate = 0 + if total_contract_amount > 0: + avg_receipt_rate = float( + (total_receipt_amount / total_contract_amount) * 100 + ) + + avg_payment_rate = 0 + total_payable = sum(p.payable_amount or Decimal(0) for p in projects) + if total_payable > 0: + avg_payment_rate = float( + (total_payment_amount / total_payable) * 100 + ) + + avg_progress = 0 + if total_count > 0: + avg_progress = float( + sum( + p.cumulative_progress or Decimal(0) for p in projects + ) / total_count + ) + + return { + "success": True, + "message": "统计成功", + "data": { + "total_count": total_count, + "total_contract_amount": float(total_contract_amount), + "total_receipt_amount": float(total_receipt_amount), + "total_payment_amount": float(total_payment_amount), + " avg_receipt_completion_rate": avg_receipt_rate, + "avg_payment_completion_rate": avg_payment_rate, + "avg_cumulative_progress": avg_progress, + }, + "error_code": None, + } + + +@router.get("/statistics/group", response_model=dict) +async def get_group_statistics( + group_by: str = Query(..., pattern="^(engineering_type|project_department)$"), + signing_date_start: Optional[date] = None, + signing_date_end: Optional[date] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + if signing_date_start: + query = query.where(Project.signing_date >= signing_date_start) + if signing_date_end: + query = query.where(Project.signing_date <= signing_date_end) + + result = await db.execute(query) + projects = result.scalars().all() + + groups = {} + for p in projects: + group_value = getattr(p, group_by) + if group_value not in groups: + groups[group_value] = { + "count": 0, + "total_contract_amount": Decimal(0), + "total_receipt_amount": Decimal(0), + "total_payment_amount": Decimal(0), + } + groups[group_value]["count"] += 1 + groups[group_value]["total_contract_amount"] += p.contract_amount or Decimal(0) + groups[group_value]["total_receipt_amount"] += p.actual_receipt_amount or Decimal(0) + groups[group_value]["total_payment_amount"] += p.actual_payment_amount or Decimal(0) + + result_list = [] + for key, value in groups.items(): + if group_by == "engineering_type": + item = {"engineering_type": key} + else: + item = {"project_department": key} + item.update({ + "count": value["count"], + "total_contract_amount": float(value["total_contract_amount"]), + "total_receipt_amount": float(value["total_receipt_amount"]), + "total_payment_amount": float(value["total_payment_amount"]), + }) + result_list.append(item) + + return { + "success": True, + "message": "统计成功", + "data": result_list, + "error_code": None, + } + + +@router.get("/statistics/timeline", response_model=dict) +async def get_timeline_statistics( + time_field: str = Query(..., pattern="^(signing_date|start_date|planned_end_date)$"), + group_by: str = Query("month", pattern="^(day|month|year)$"), + engineering_type: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + query = select(Project) + + if engineering_type: + query = query.where(Project.engineering_type == engineering_type) + if start_date: + date_column = getattr(Project, time_field) + query = query.where(date_column >= start_date) + if end_date: + date_column = getattr(Project, time_field) + query = query.where(date_column <= end_date) + + result = await db.execute(query) + projects = result.scalars().all() + + groups = {} + for p in projects: + date_value = getattr(p, time_field) + if not date_value: + continue + + if group_by == "day": + key = date_value.strftime("%Y-%m-%d") + elif group_by == "month": + key = date_value.strftime("%Y-%m") + else: + key = str(date_value.year) + + if key not in groups: + groups[key] = {"count": 0, "total_contract_amount": Decimal(0)} + groups[key]["count"] += 1 + groups[key]["total_contract_amount"] += p.contract_amount or Decimal(0) + + result_list = [] + for key in sorted(groups.keys()): + value = groups[key] + if group_by == "day": + item = {"day": key} + elif group_by == "month": + item = {"month": key} + else: + item = {"year": key} + item.update({ + "count": value["count"], + "total_contract_amount": float(value["total_contract_amount"]), + }) + result_list.append(item) + + return { + "success": True, + "message": "统计成功", + "data": result_list, + "error_code": None, + }