""" 项目管理系统 API(依据产品文档与 API 文档) 功能:认证、项目 CRUD、操作日志;数据持久化到 MySQL。 """ from typing import Optional from fastapi import FastAPI, Header, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from pydantic import BaseModel from . import auth from . import projects_repo app = FastAPI(title="项目管理系统 API", version="1.0.0") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) # ---------- 请求体模型 ---------- class LoginBody(BaseModel): username: Optional[str] = None password: Optional[str] = None class ProjectListBody(BaseModel): page: Optional[int] = 1 pageSize: Optional[int] = 50 searchType: Optional[str] = None keyword: Optional[str] = None progress: Optional[str] = None cost: Optional[str] = None dateFilterType: Optional[str] = None # signDate | startDate | plannedCompletionDate | actualCompletionDate dateFrom: Optional[str] = None dateTo: Optional[str] = None dateAbnormal: Optional[bool] = None # True:只查日期异常项目 amountMin: Optional[float] = None # 合同金额(万元)下限 amountMax: Optional[float] = None # 合同金额(万元)上限 class ProjectDetailBody(BaseModel): id: str class ContractCreate(BaseModel): contractCode: Optional[str] = None projectName: Optional[str] = None class ProjectCreateBody(BaseModel): contract: Optional[ContractCreate] = None costControl: Optional[dict] = None receivable: Optional[dict] = None payable: Optional[dict] = None other: Optional[dict] = None class ProjectUpdateBody(BaseModel): id: str contract: Optional[dict] = None costControl: Optional[dict] = None receivable: Optional[dict] = None payable: Optional[dict] = None other: Optional[dict] = None class ProjectLogsBody(BaseModel): id: str page: Optional[int] = 1 pageSize: Optional[int] = 20 def _get_current_user(authorization: Optional[str] = Header(None)) -> dict: """从 Authorization Bearer token 获取当前用户,否则 401。""" if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="未登录") token = authorization[7:].strip() user = auth.get_user_by_token(token) if not user: raise HTTPException(status_code=401, detail="未登录") return user # ---------- 认证 ---------- @app.post("/api/auth/login") def login(body: LoginBody): if body.username is None or (isinstance(body.username, str) and body.username.strip() == ""): raise HTTPException(status_code=400, detail="缺少 username") if body.password is None or (isinstance(body.password, str) and body.password.strip() == ""): raise HTTPException(status_code=400, detail="缺少 password") try: user = auth.verify_login(body.username.strip(), body.password) except Exception as e: import logging logging.getLogger("app").exception("登录校验异常") return JSONResponse(status_code=401, content={"code": 401, "message": "账号或密码错误"}) if not user: return JSONResponse(status_code=401, content={"code": 401, "message": "账号或密码错误"}) token = auth.create_token(user) return { "token": token, "user": { "id": user["id"], "username": user["username"], "role": user["role"], "displayName": user["displayName"], }, } # ---------- 项目列表 ---------- @app.post("/api/projects/list") def project_list(body: ProjectListBody, authorization: Optional[str] = Header(None)): _get_current_user(authorization) page = body.page or 1 page_size = body.pageSize or 50 list_, total = projects_repo.list_projects( page=page, page_size=page_size, search_type=body.searchType, keyword=body.keyword, progress=body.progress, cost=body.cost, date_filter_type=body.dateFilterType, date_from=body.dateFrom, date_to=body.dateTo, date_abnormal=body.dateAbnormal, amount_min=body.amountMin, amount_max=body.amountMax, ) return {"list": list_, "total": total, "page": page, "pageSize": page_size} # ---------- 项目详情 ---------- @app.post("/api/projects/detail") def project_detail(body: ProjectDetailBody, authorization: Optional[str] = Header(None)): _get_current_user(authorization) project = projects_repo.get_project(body.id) if not project: return JSONResponse(status_code=404, content={"code": 404, "message": "项目不存在"}) return project # ---------- 新建项目 ---------- @app.post("/api/projects/create", status_code=201) def project_create(body: ProjectCreateBody, authorization: Optional[str] = Header(None)): user = _get_current_user(authorization) if user["role"] != "市场部": return JSONResponse(status_code=403, content={"code": 403, "message": "无权限"}) contract = body.contract if contract is None: return JSONResponse(status_code=400, content={"code": 400, "message": "合同编号不能为空"}) contract_code = getattr(contract, "contractCode", None) project_name = getattr(contract, "projectName", None) if contract_code is None or (isinstance(contract_code, str) and contract_code.strip() == ""): return JSONResponse(status_code=400, content={"code": 400, "message": "合同编号不能为空"}) if project_name is None or (isinstance(project_name, str) and project_name.strip() == ""): return JSONResponse(status_code=400, content={"code": 400, "message": "项目名称不能为空"}) contract_dict = body.contract.model_dump() if body.contract else {} pid = projects_repo.create_project( contract=contract_dict, cost_control=body.costControl, receivable=body.receivable, payable=body.payable, other=body.other, ) return {"id": pid, "message": "创建成功"} # ---------- 更新项目 ---------- @app.post("/api/projects/update") def project_update(body: ProjectUpdateBody, authorization: Optional[str] = Header(None)): user = _get_current_user(authorization) if not projects_repo.project_exists(body.id): return JSONResponse(status_code=404, content={"code": 404, "message": "项目不存在"}) projects_repo.update_project( body.id, contract=body.contract, cost_control=body.costControl, receivable=body.receivable, payable=body.payable, other=body.other, ) summary_parts = [] if body.contract is not None: summary_parts.append("合同信息") if body.costControl is not None: summary_parts.append("成本控制") if body.receivable is not None: summary_parts.append("应收款") if body.payable is not None: summary_parts.append("应付款") if body.other is not None: summary_parts.append("其他") summary = "、".join(summary_parts) if summary_parts else "项目信息" projects_repo.insert_operation_log( project_id=body.id, operator_id=user["id"], summary=summary, detail=None, ) return {"id": body.id, "message": "保存成功"} # ---------- 操作日志 ---------- @app.post("/api/projects/logs") def project_logs(body: ProjectLogsBody, authorization: Optional[str] = Header(None)): _get_current_user(authorization) if not projects_repo.project_exists(body.id): return JSONResponse(status_code=404, content={"code": 404, "message": "项目不存在"}) page = body.page or 1 page_size = body.pageSize or 20 list_, total = projects_repo.list_operation_logs(body.id, page=page, page_size=page_size) return {"list": list_, "total": total, "page": page, "pageSize": page_size}