From fc3cec1b1e568da9c0184a048a73648a84b14ed4 Mon Sep 17 00:00:00 2001 From: xsl Date: Mon, 26 Jan 2026 14:14:16 +0800 Subject: [PATCH] =?UTF-8?q?[backend]=20fix:=20=E4=BF=AE=E5=A4=8Drequire=5F?= =?UTF-8?q?admin=E9=94=99=E8=AF=AF=E7=A0=81=E5=92=8C=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.coverage | Bin 53248 -> 53248 bytes .../__pycache__/auth.cpython-310.pyc | Bin 2320 -> 2327 bytes backend/src/middleware/auth.py | 7 +- .../__pycache__/projects.cpython-310.pyc | Bin 8946 -> 10115 bytes backend/src/routes/projects.py | 77 +++ backend/src/routes/projects.py.backup2 | 438 ++++++++++++++++++ 6 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 backend/src/routes/projects.py.backup2 diff --git a/backend/.coverage b/backend/.coverage index fe63f52c40d0debaa5e0709443f23192c1f73c25..156d3d68abc4a60f3609f29ecb65a3fba11795c0 100644 GIT binary patch delta 158 zcmZozz}&Eac>`O6%6SI<&-}0WFY%w^-^#y|e-VE-zdOGwzcjxP-%q|bd{6jp@SWc* zD6o`o^6kC~7bi|uM$SeSfqx2d|Nd{?&GpTc0SNvxf=D3vz{^m>%)s!26~qP+aG3+~ u`XB`l_WQ9+?&)`z5MyRx`O6%54Vz&-}0WFY}+~-^Ra+e=&a#e=xrxzdFAhKP%r4z7Kp)_-=0& z6xhf&`Fvl6r8y@HBWHoYKZUq||F`bu`ew?&z|fG-@Iab@;ebAf4I+N<0|_wrSH%dT o|CcIF?(TON7i9+O*5Uj2^#8?r9tWU=O~ar0Ke#u)>KAhW0MY6%c>n+a diff --git a/backend/src/middleware/__pycache__/auth.cpython-310.pyc b/backend/src/middleware/__pycache__/auth.cpython-310.pyc index 16d4d02498d920735c36c416b87919545e89ca17..b65822cf934b9622359d7b3080a60de9e7537815 100644 GIT binary patch delta 104 zcmbOrG+l@u6KA+I`bmEL>bDLkzoKR#0lqnJd5|eA$4cUv#L0pT;huLp2T1=kDQ3L=< CS|Kn1 delta 120 zcmbO(G(m_jpO=@50SF|Xlx6PX-N^Tmjj?zVyMZwaQ0y=e7s~;OM#dV3EQS=uY~~`~ z62=myEanuZ6y{#0CZKpQgC>h#kpWQE`Apl#T+URc2;XM^KSRc_s`z@{xG4KnZlgLj>6nJ+7k z98y@p3YW+83jGyZ7$9pz;VK!U_@k>NSuvYNv9O`I?W*kF?!gjTFfb7~Gc5W7BV&=# zuq>ps-MZdX!F7uQn=ZzmTzXe>rM&*gNMtw=9*T;-pd1^NA{0tr?iTeL#Vh%PETQAS z`KQ%tPP&gOF1iopoTKP@!X)7W{^stt1(g|+_-sM%emgL*-Dk}5AG0!G%m&+E%RRQo z_LrQ+Z}ukOA}(ch!k2hA%L{XHmd!!AdK^wXuW4`xFKP#2KK`|~2u1WvyEj^8ORVn%ieKAgLdgOvIfp@p!3-{yMY8m{HG)xAxQ6AB3%7sDbf zBz5pvjgqo~qK$-?2sBd}D+Y`>nk`Gp6aUJw3Qclr$MP(t?e=W?Km|uBV~mc4e_KX7 z+f{bPkfkVOI)&I!sDqT$Bb-hVBt?{ZJhMI3csj+z=FOax6DrS_AyZF-^5Wn5-g-Gj zb*2f%`d=q&hLG~s%qm}pdI1YA#D6GQgXW}@McLg14e7o&N#T2Ac-} delta 1259 zcmaKo&refX6vulXv_b=q{-CxM+X9tBDU>3JBK1dAK;4KUlgz}Vd2%i&0b6fh8?8T3 zi5cF1m4LjESpZHsfaIo+nBn(VM)_J@=gR{oeC#;n3fM zYO1!@YLbh+&7IWyuB#iF{xq_{uu*X}M7R?|+zzdL-Gl`?pmWY#U_Z6-^HzxR^tXr6|fx!z&o#i8{Df#F+&{F6~5UWu+z|OHgvnn zbh}B%DRX4`Un{u_V}5O6P|c9M~M-Mk_GrQ*SR3~ zu-UK^F{*YGjuB{{X62;`|MB#)^BAm&6^FdH87|dESQrCp%;8`r(^8e}RW4PURA&7z zkD3~{ULf^Fi7-A={r*`x@~@bn=(2=Ms>KzwH~7jG);fRNw-g>jL3$aOL=$OuuH2@S z)U?DD;5 zy=9%$K1lEr))ZPP*CugP{EP= 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, + }