Files
ocean/backend/src/app.py
T
2026-01-31 23:41:19 +08:00

214 lines
7.8 KiB
Python

"""
项目管理系统 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}