214 lines
7.8 KiB
Python
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}
|