后端第一步开发完成,第一版回归测试完成
This commit is contained in:
+83
-97
@@ -1,37 +1,22 @@
|
||||
"""
|
||||
项目管理系统 API(依据产品文档与 API 文档)
|
||||
TDD:路由按测试用例实现
|
||||
功能:认证、项目 CRUD、操作日志;数据持久化到 MySQL。
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
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=["*"])
|
||||
|
||||
# ---------- 内存存储(TDD 绿阶段最小实现,后续可换 DB)----------
|
||||
# token -> role;测试用:valid-token / market-token -> 市场部, engineer-token -> 工程部
|
||||
TOKEN_ROLE: dict[str, str] = {
|
||||
"valid-token": "市场部",
|
||||
"market-token": "市场部",
|
||||
"engineer-token": "工程部",
|
||||
}
|
||||
# 合法用户:仅 market / valid-password 登录成功
|
||||
VALID_USER = {"username": "market", "password": "valid-password", "role": "市场部", "display_name": "市场部用户"}
|
||||
# 项目 id -> 项目数据(含 contract, costControl, receivable, payable, other)
|
||||
PROJECTS: dict[str, dict[str, Any]] = {}
|
||||
# 操作日志 project_id -> list of log
|
||||
OPERATION_LOGS: dict[str, list[dict]] = {}
|
||||
|
||||
# 不预置项目,测试通过 create 或 fixture 造数;列表无数据时返回 []
|
||||
|
||||
|
||||
# ---------- 请求/响应模型(最小)----------
|
||||
# ---------- 请求体模型 ----------
|
||||
class LoginBody(BaseModel):
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
@@ -44,6 +29,10 @@ class ProjectListBody(BaseModel):
|
||||
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:只查日期异常项目
|
||||
|
||||
|
||||
class ProjectDetailBody(BaseModel):
|
||||
@@ -78,20 +67,15 @@ class ProjectLogsBody(BaseModel):
|
||||
pageSize: Optional[int] = 20
|
||||
|
||||
|
||||
def _get_role(authorization: Optional[str] = Header(None)) -> str:
|
||||
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()
|
||||
role = TOKEN_ROLE.get(token)
|
||||
if role is None:
|
||||
user = auth.get_user_by_token(token)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
return role
|
||||
|
||||
|
||||
def _get_token(authorization: Optional[str] = Header(None)) -> str:
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
return authorization[7:].strip()
|
||||
return user
|
||||
|
||||
|
||||
# ---------- 认证 ----------
|
||||
@@ -101,16 +85,22 @@ def login(body: LoginBody):
|
||||
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")
|
||||
if body.username != VALID_USER["username"] or body.password != VALID_USER["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": "账号或密码错误"})
|
||||
token = "valid-token"
|
||||
if not user:
|
||||
return JSONResponse(status_code=401, content={"code": 401, "message": "账号或密码错误"})
|
||||
token = auth.create_token(user)
|
||||
return {
|
||||
"token": token,
|
||||
"user": {
|
||||
"id": "user-market",
|
||||
"username": VALID_USER["username"],
|
||||
"role": VALID_USER["role"],
|
||||
"displayName": VALID_USER["display_name"],
|
||||
"id": user["id"],
|
||||
"username": user["username"],
|
||||
"role": user["role"],
|
||||
"displayName": user["displayName"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -118,50 +108,39 @@ def login(body: LoginBody):
|
||||
# ---------- 项目列表 ----------
|
||||
@app.post("/api/projects/list")
|
||||
def project_list(body: ProjectListBody, authorization: Optional[str] = Header(None)):
|
||||
_get_role(authorization)
|
||||
_get_current_user(authorization)
|
||||
page = body.page or 1
|
||||
page_size = body.pageSize or 20
|
||||
items = list(PROJECTS.values())
|
||||
# 列表项:id, projectName, contractCode, progress, cost, updatedAt
|
||||
list_ = [
|
||||
{
|
||||
"id": p["id"],
|
||||
"projectName": (p.get("contract") or {}).get("projectName") or "",
|
||||
"contractCode": (p.get("contract") or {}).get("contractCode") or "",
|
||||
"progress": p.get("progress") or "",
|
||||
"cost": p.get("cost") or "",
|
||||
"updatedAt": p.get("updatedAt") or "",
|
||||
}
|
||||
for p in items
|
||||
]
|
||||
return {"list": list_, "total": len(list_), "page": page, "pageSize": page_size}
|
||||
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,
|
||||
)
|
||||
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_role(authorization)
|
||||
pid = body.id
|
||||
if pid not in PROJECTS:
|
||||
_get_current_user(authorization)
|
||||
project = projects_repo.get_project(body.id)
|
||||
if not project:
|
||||
return JSONResponse(status_code=404, content={"code": 404, "message": "项目不存在"})
|
||||
p = PROJECTS[pid]
|
||||
return {
|
||||
"id": p["id"],
|
||||
"contract": p.get("contract") or {},
|
||||
"costControl": p.get("costControl") or {},
|
||||
"receivable": p.get("receivable") or {},
|
||||
"payable": p.get("payable") or {},
|
||||
"other": p.get("other") or {},
|
||||
"createdAt": p.get("createdAt", ""),
|
||||
"updatedAt": p.get("updatedAt", ""),
|
||||
}
|
||||
return project
|
||||
|
||||
|
||||
# ---------- 新建项目 ----------
|
||||
@app.post("/api/projects/create", status_code=201)
|
||||
def project_create(body: ProjectCreateBody, authorization: Optional[str] = Header(None)):
|
||||
role = _get_role(authorization)
|
||||
if role != "市场部":
|
||||
user = _get_current_user(authorization)
|
||||
if user["role"] != "市场部":
|
||||
return JSONResponse(status_code=403, content={"code": 403, "message": "无权限"})
|
||||
contract = body.contract
|
||||
if contract is None:
|
||||
@@ -172,52 +151,59 @@ def project_create(body: ProjectCreateBody, authorization: Optional[str] = Heade
|
||||
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": "项目名称不能为空"})
|
||||
pid = str(uuid.uuid4())
|
||||
now = datetime.utcnow().isoformat() + "Z"
|
||||
PROJECTS[pid] = {
|
||||
"id": pid,
|
||||
"contract": body.contract.model_dump() if body.contract else {},
|
||||
"costControl": body.costControl or {},
|
||||
"receivable": body.receivable or {},
|
||||
"payable": body.payable or {},
|
||||
"other": body.other or {},
|
||||
"createdAt": now,
|
||||
"updatedAt": now,
|
||||
}
|
||||
OPERATION_LOGS[pid] = []
|
||||
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)):
|
||||
_get_role(authorization)
|
||||
pid = body.id
|
||||
if pid not in PROJECTS:
|
||||
user = _get_current_user(authorization)
|
||||
if not projects_repo.project_exists(body.id):
|
||||
return JSONResponse(status_code=404, content={"code": 404, "message": "项目不存在"})
|
||||
p = PROJECTS[pid]
|
||||
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:
|
||||
p["contract"] = {**(p.get("contract") or {}), **body.contract}
|
||||
summary_parts.append("合同信息")
|
||||
if body.costControl is not None:
|
||||
p["costControl"] = {**(p.get("costControl") or {}), **body.costControl}
|
||||
summary_parts.append("成本控制")
|
||||
if body.receivable is not None:
|
||||
p["receivable"] = {**(p.get("receivable") or {}), **body.receivable}
|
||||
summary_parts.append("应收款")
|
||||
if body.payable is not None:
|
||||
p["payable"] = {**(p.get("payable") or {}), **body.payable}
|
||||
summary_parts.append("应付款")
|
||||
if body.other is not None:
|
||||
p["other"] = {**(p.get("other") or {}), **body.other}
|
||||
p["updatedAt"] = datetime.utcnow().isoformat() + "Z"
|
||||
return {"id": pid, "message": "保存成功"}
|
||||
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_role(authorization)
|
||||
pid = body.id
|
||||
if pid not in PROJECTS:
|
||||
_get_current_user(authorization)
|
||||
if not projects_repo.project_exists(body.id):
|
||||
return JSONResponse(status_code=404, content={"code": 404, "message": "项目不存在"})
|
||||
logs = OPERATION_LOGS.get(pid, [])
|
||||
page = body.page or 1
|
||||
page_size = body.pageSize or 20
|
||||
return {"list": logs, "total": len(logs), "page": page, "pageSize": page_size}
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user