后端第一步开发完成,第一版回归测试完成

This commit is contained in:
Your Name
2026-01-31 19:09:48 +08:00
parent 11cef531bd
commit c9e75030af
26 changed files with 1686 additions and 184 deletions
+83 -97
View File
@@ -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}