# 海洋项目管理系统 - 后端实现计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 构建完整的后端API系统,包括认证、用户管理、项目管理和统计功能 **Architecture:** 基于FastAPI的异步后端服务,使用SQLAlchemy 2.0 ORM操作MySQL数据库,JWT Token认证,Pydantic v2进行数据验证。 **Tech Stack:** - FastAPI 0.104.1 - Web框架 - SQLAlchemy 2.0.23 - ORM - aiomysql 0.2.0 - MySQL异步驱动 - PyJWT 2.8.0 - JWT认证 - bcrypt 4.1.1 - 密码加密 - Pydantic 2.5.0 - 数据验证 --- ## Task 1: 创建项目基础配置文件 **Files:** - Create: `backend/requirements.txt` - Create: `backend/config/__init__.py` - Create: `backend/config/settings.py` - Create: `backend/config/database.py` **Step 1: 创建依赖文件** ```python # backend/requirements.txt fastapi==0.104.1 uvicorn[standard]==0.24.0 sqlalchemy==2.0.23 aiomysql==0.2.0 pydantic==2.5.0 pydantic-settings==2.1.0 pyjwt==2.8.0 bcrypt==4.1.1 python-multipart==0.0.6 ``` **Step 2: 创建配置类** ```python # backend/config/settings.py from pydantic_settings import BaseSettings from functools import lru_cache class Settings(BaseSettings): # 应用配置 APP_NAME: str = "海洋项目管理系统" APP_VERSION: str = "1.0.0" DEBUG: bool = True # 数据库配置 DB_HOST: str = "localhost" DB_PORT: int = 3306 DB_USER: str = "root" DB_PASSWORD: str = "rootpassword" DB_NAME: str = "project_manager" DB_CHARSET: str = "utf8mb4" # JWT配置 SECRET_KEY: str = "your-secret-key-change-this-in-production" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24小时 # CORS配置 CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://127.0.0.1:3000"] class Config: env_file = ".env" case_sensitive = True @lru_cache() def get_settings(): return Settings() ``` **Step 3: 创建数据库配置** ```python # backend/config/database.py from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.orm import declarative_base from .settings import get_settings settings = get_settings() # 构建数据库连接URL DATABASE_URL = ( f"mysql+aiomysql://{settings.DB_USER}:{settings.DB_PASSWORD}" f"@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" f"?charset={settings.DB_CHARSET}" ) # 创建异步引擎 engine = create_async_engine(DATABASE_URL, echo=settings.DEBUG, future=True) # 创建会话工厂 AsyncSessionLocal = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) # 创建基类 Base = declarative_base() # 依赖注入:获取数据库会话 async def get_db(): async with AsyncSessionLocal() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() # 初始化数据库表 async def init_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) ``` **Step 4: 创建配置包初始化文件** ```python # backend/config/__init__.py from .settings import get_settings, Settings from .database import get_db, init_db, Base, engine, AsyncSessionLocal __all__ = [ "get_settings", "Settings", "get_db", "init_db", "Base", "engine", "AsyncSessionLocal", ] ``` **Step 5: 安装依赖** Run: `cd backend && pip install -r requirements.txt -r requirements-test.txt` Expected: 所有依赖安装成功 **Step 6: 提交** ```bash git add backend/requirements.txt backend/config/ git commit -m "[backend] feat: 添加项目基础配置" ``` --- ## Task 2: 创建工具函数模块 **Files:** - Create: `backend/src/utils/__init__.py` - Create: `backend/src/utils/password.py` - Create: `backend/src/utils/jwt.py` **Step 1: 创建密码工具** ```python # backend/src/utils/password.py import bcrypt def hash_password(password: str) -> str: """哈希密码""" salt = bcrypt.gensalt(rounds=12) hashed = bcrypt.hashpw(password.encode("utf-8"), salt) return hashed.decode("utf-8") def verify_password(password: str, hashed_password: str) -> bool: """验证密码""" return bcrypt.checkpw( password.encode("utf-8"), hashed_password.encode("utf-8") ) ``` **Step 2: 创建JWT工具** ```python # backend/src/utils/jwt.py from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from config.settings import get_settings settings = get_settings() def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: """创建访问令牌""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta( minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES ) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode( to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM ) return encoded_jwt def decode_access_token(token: str) -> Optional[dict]: """解码访问令牌""" try: payload = jwt.decode( token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] ) return payload except JWTError: return None ``` **Step 3: 创建工具包初始化文件** ```python # backend/src/utils/__init__.py from .password import hash_password, verify_password from .jwt import create_access_token, decode_access_token __all__ = [ "hash_password", "verify_password", "create_access_token", "decode_access_token", ] ``` **Step 4: 测试密码工具** Run: `cd backend && python -c "from src.utils.password import hash_password; print(hash_password('test123'))"` Expected: 输出哈希后的密码 **Step 5: 提交** ```bash git add backend/src/utils/ git commit -m "[backend] feat: 添加密码和JWT工具函数" ``` --- ## Task 3: 创建数据库模型 **Files:** - Create: `backend/src/models/__init__.py` - Create: `backend/src/models/user.py` - Create: `backend/src/models/project.py` **Step 1: 创建用户模型** ```python # backend/src/models/user.py from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum from sqlalchemy.sql import func from config.database import Base class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, autoincrement=True, comment="用户ID") username = Column(String(50), unique=True, nullable=False, comment="用户名") password_hash = Column(String(255), nullable=False, comment="密码哈希") real_name = Column(String(100), nullable=False, comment="真实姓名") department = Column(String(50), nullable=False, comment="部门") role = Column(Enum("admin", "market", "other"), nullable=False, comment="角色") email = Column(String(100), unique=True, comment="邮箱") phone = Column(String(20), comment="电话") is_active = Column(Boolean, default=True, comment="是否激活") created_at = Column( DateTime, server_default=func.now(), comment="创建时间", ) updated_at = Column( DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间", ) ``` **Step 2: 创建项目模型** ```python # backend/src/models/project.py from sqlalchemy import Column, Integer, String, Text, Date, Numeric, DateTime, Enum from sqlalchemy.sql import func from config.database import Base class Project(Base): __tablename__ = "projects" id = Column(Integer, primary_key=True, autoincrement=True, comment="项目ID") # 基础信息 project_no = Column(String(50), unique=True, nullable=False, comment="合同编号") power_contract_no = Column(String(100), comment="供电局项目合同编号") name = Column(String(200), nullable=False, comment="项目名称") subitem_count = Column(Integer, default=0, comment="子项个数") subitem_code = Column(String(50), comment="子项编码") total_investment = Column(Numeric(15, 2), comment="项目总投资(万元)") contract_amount = Column(Numeric(15, 2), comment="中标合同金额(万元)") warranty_ratio = Column(Numeric(5, 2), comment="质保金比例") settlement_amount = Column(Numeric(15, 2), comment="结算金额(万元)") total_cost_estimated = Column(Numeric(15, 2), comment="总成本测算") voltage_level = Column(String(50), comment="工程电压等级") engineering_type = Column(String(50), comment="工程类别") owner_unit = Column(String(200), comment="业主单位") owner_contact = Column(String(200), comment="业主联系人及电话") bidding_type = Column(String(50), comment="中标形式") signing_date = Column(Date, comment="签订日期") start_date = Column(Date, comment="开工日期") planned_end_date = Column(Date, comment="计划竣工日期") actual_end_date = Column(Date, comment="实际竣工日期") # 质保信息 warranty_amount = Column(Numeric(15, 2), default=0, comment="质保金(万元)") warranty_expiry_date = Column(Date, comment="质保期截止日") actual_warranty_refund_date = Column(Date, comment="实际退质保金日期") # 项目信息 project_department = Column(String(100), comment="所属项目部") project_leader = Column(String(200), comment="项目负责人及电话") payment_method = Column(Text, comment="工程款拨付方式") # 成本控制 total_cost_control = Column(Numeric(15, 2), comment="总体成本(控制)") is_adjusted = Column(Enum("是", "否"), default="否", comment="是否调整") labor_cost_control = Column(Numeric(15, 2), comment="人工成本(控制)") labor_cost_planned = Column(Numeric(15, 2), comment="农民工工资(计划)") labor_cost_paid = Column(Numeric(15, 2), comment="农民工工资(实付)") material_cost_control = Column(Numeric(15, 2), comment="乙供材料费(控制)") material_cost_payable = Column(Numeric(15, 2), comment="应付材料费") material_cost_actual = Column(Numeric(15, 2), comment="实际发生材料费") material_cost_paid = Column(Numeric(15, 2), comment="实际支付材料费") other_cost_control = Column(Numeric(15, 2), comment="其他费用(控制)") other_cost_payable = Column(Numeric(15, 2), comment="应付其他费") other_cost_actual = Column(Numeric(15, 2), comment="实际其他费用") # 财务信息 tax_amount = Column(Numeric(15, 2), comment="税金") profit = Column(Numeric(15, 2), comment="利润(万元)") actual_profit = Column(Numeric(15, 2), comment="实际利润(万元)") cost_settlement_amount = Column(Numeric(15, 2), comment="成本结算金额(万元)") cumulative_progress = Column(Numeric(5, 2), comment="累计进度") receivable_amount = Column(Numeric(15, 2), comment="应收款(完成进度款)") invoice_amount = Column(Numeric(15, 2), comment="开票金额(万元)") actual_receipt_amount = Column(Numeric(15, 2), comment="实际收款金额(万元)") receipt_completion_rate = Column(Numeric(5, 2), comment="实际收款完成率") payable_amount = Column(Numeric(15, 2), comment="应付款金额(万元)") actual_payment_amount = Column(Numeric(15, 2), comment="实际付款金额(万元)") unpaid_amount = Column(Numeric(15, 2), comment="未收款(万元)") payment_completion_rate = Column(Numeric(5, 2), comment="实际付款完成率") # 民工工资 labor_debt_amount = Column(Numeric(15, 2), comment="民工工资清欠金额(万元)") # 结算信息 settlement_cost_amount = Column(Numeric(15, 2), comment="结算后成本测算金额") settlement_labor_cost = Column(Numeric(15, 2), comment="结算人工费") settlement_material_cost = Column(Numeric(15, 2), comment="结算材料费") settlement_other_cost = Column(Numeric(15, 2), comment="结算其他费") # 统计信息 due_settlement_count = Column(Integer, comment="到期应结算项目个数") unsettlement_count = Column(Integer, comment="到期未完成结算个数") # 其他 problems = Column(Text, comment="存在的问题") suggestions = Column(Text, comment="建议措施") remarks = Column(Text, comment="备注") # 系统字段 created_by = Column(Integer, nullable=False, comment="创建人ID") created_at = Column( DateTime, server_default=func.now(), comment="创建时间", ) updated_at = Column( DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间", ) ``` **Step 3: 创建模型包初始化文件** ```python # backend/src/models/__init__.py from .user import User from .project import Project __all__ = ["User", "Project"] ``` **Step 4: 提交** ```bash git add backend/src/models/ git commit -m "[backend] feat: 添加用户和项目数据模型" ``` --- ## Task 4: 创建Pydantic Schema模型 **Files:** - Create: `backend/src/schemas/__init__.py` - Create: `backend/src/schemas/user.py` - Create: `backend/src/schemas/project.py` - Create: `backend/src/schemas/auth.py` **Step 1: 创建用户Schema** ```python # backend/src/schemas/user.py from pydantic import BaseModel, EmailStr, Field from typing import Optional from datetime import datetime # 用户基础信息 class UserBase(BaseModel): username: str = Field(..., min_length=3, max_length=50) real_name: str = Field(..., max_length=100) department: str = Field(..., max_length=50) role: str = Field(..., pattern="^(admin|market|other)$") email: Optional[EmailStr] = None phone: Optional[str] = Field(None, max_length=20) # 创建用户请求 class UserCreate(UserBase): password: str = Field(..., min_length=6) # 更新用户请求 class UserUpdate(BaseModel): real_name: Optional[str] = Field(None, max_length=100) email: Optional[EmailStr] = None phone: Optional[str] = Field(None, max_length=20) is_active: Optional[bool] = None # 重置密码请求 class PasswordReset(BaseModel): new_password: str = Field(..., min_length=6) # 用户响应 class UserResponse(UserBase): id: int is_active: bool created_at: datetime updated_at: Optional[datetime] class Config: from_attributes = True # 用户列表响应 class UserListResponse(BaseModel): items: list[UserResponse] total: int page: int page_size: int ``` **Step 2: 创建认证Schema** ```python # backend/src/schemas/auth.py from pydantic import BaseModel, Field from typing import Optional from .user import UserResponse # 登录请求 class LoginRequest(BaseModel): username: str = Field(..., min_length=3) password: str = Field(..., min_length=6) # 登录响应 class LoginResponse(BaseModel): token: str token_type: str = "bearer" user: UserResponse ``` **Step 3: 创建项目Schema** ```python # backend/src/schemas/project.py from pydantic import BaseModel, Field from typing import Optional from datetime import date, datetime from decimal import Decimal # 项目基础信息 class ProjectBase(BaseModel): project_no: str = Field(..., max_length=50) name: str = Field(..., max_length=200) engineering_type: str = Field(..., max_length=50) contract_amount: Optional[Decimal] = Field(None, ge=0) # 创建项目请求 class ProjectCreate(ProjectBase): power_contract_no: Optional[str] = Field(None, max_length=100) subitem_count: Optional[int] = Field(None, ge=0) subitem_code: Optional[str] = Field(None, max_length=50) total_investment: Optional[Decimal] = Field(None, ge=0) warranty_ratio: Optional[Decimal] = None settlement_amount: Optional[Decimal] = None total_cost_estimated: Optional[Decimal] = None voltage_level: Optional[str] = Field(None, max_length=50) owner_unit: Optional[str] = Field(None, max_length=200) owner_contact: Optional[str] = Field(None, max_length=200) bidding_type: Optional[str] = Field(None, max_length=50) signing_date: Optional[date] = None start_date: Optional[date] = None planned_end_date: Optional[date] = None project_department: Optional[str] = Field(None, max_length=100) project_leader: Optional[str] = Field(None, max_length=200) payment_method: Optional[str] = None total_cost_control: Optional[Decimal] = None labor_cost_control: Optional[Decimal] = None material_cost_control: Optional[Decimal] = None other_cost_control: Optional[Decimal] = None remarks: Optional[str] = None # 更新项目请求 class ProjectUpdate(BaseModel): # 基础信息 name: Optional[str] = Field(None, max_length=200) engineering_type: Optional[str] = Field(None, max_length=50) contract_amount: Optional[Decimal] = Field(None, ge=0) # 财务信息 total_investment: Optional[Decimal] = None settlement_amount: Optional[Decimal] = None actual_receipt_amount: Optional[Decimal] = None actual_payment_amount: Optional[Decimal] = None invoice_amount: Optional[Decimal] = None payable_amount: Optional[Decimal] = None cumulative_progress: Optional[Decimal] = None # 成本信息 labor_cost_paid: Optional[Decimal] = None material_cost_actual: Optional[Decimal] = None material_cost_paid: Optional[Decimal] = None other_cost_actual: Optional[Decimal] = None tax_amount: Optional[Decimal] = None profit: Optional[Decimal] = None # 其他 remarks: Optional[str] = None problems: Optional[str] = None suggestions: Optional[str] = None # 项目响应 class ProjectResponse(BaseModel): id: int project_no: str power_contract_no: Optional[str] name: str subitem_count: Optional[int] subitem_code: Optional[str] total_investment: Optional[Decimal] contract_amount: Optional[Decimal] warranty_ratio: Optional[Decimal] settlement_amount: Optional[Decimal] total_cost_estimated: Optional[Decimal] voltage_level: Optional[str] engineering_type: Optional[str] owner_unit: Optional[str] owner_contact: Optional[str] bidding_type: Optional[str] signing_date: Optional[date] start_date: Optional[date] planned_end_date: Optional[date] actual_end_date: Optional[date] warranty_amount: Optional[Decimal] warranty_expiry_date: Optional[date] actual_warranty_refund_date: Optional[date] project_department: Optional[str] project_leader: Optional[str] payment_method: Optional[str] total_cost_control: Optional[Decimal] is_adjusted: Optional[str] labor_cost_control: Optional[Decimal] labor_cost_planned: Optional[Decimal] labor_cost_paid: Optional[Decimal] material_cost_control: Optional[Decimal] material_cost_payable: Optional[Decimal] material_cost_actual: Optional[Decimal] material_cost_paid: Optional[Decimal] other_cost_control: Optional[Decimal] other_cost_payable: Optional[Decimal] other_cost_actual: Optional[Decimal] tax_amount: Optional[Decimal] profit: Optional[Decimal] actual_profit: Optional[Decimal] cost_settlement_amount: Optional[Decimal] cumulative_progress: Optional[Decimal] receivable_amount: Optional[Decimal] invoice_amount: Optional[Decimal] actual_receipt_amount: Optional[Decimal] receipt_completion_rate: Optional[Decimal] payable_amount: Optional[Decimal] actual_payment_amount: Optional[Decimal] unpaid_amount: Optional[Decimal] payment_completion_rate: Optional[Decimal] labor_debt_amount: Optional[Decimal] settlement_cost_amount: Optional[Decimal] settlement_labor_cost: Optional[Decimal] settlement_material_cost: Optional[Decimal] settlement_other_cost: Optional[Decimal] due_settlement_count: Optional[int] unsettlement_count: Optional[int] problems: Optional[str] suggestions: Optional[str] remarks: Optional[str] created_by: int created_by_name: Optional[str] created_at: datetime updated_at: Optional[datetime] class Config: from_attributes = True # 项目列表响应 class ProjectListResponse(BaseModel): items: list[ProjectResponse] total: int page: int page_size: int ``` **Step 4: 创建Schema包初始化文件** ```python # backend/src/schemas/__init__.py from .auth import LoginRequest, LoginResponse from .user import ( UserCreate, UserUpdate, UserResponse, UserListResponse, PasswordReset, ) from .project import ( ProjectCreate, ProjectUpdate, ProjectResponse, ProjectListResponse, ) __all__ = [ "LoginRequest", "LoginResponse", "UserCreate", "UserUpdate", "UserResponse", "UserListResponse", "PasswordReset", "ProjectCreate", "ProjectUpdate", "ProjectResponse", "ProjectListResponse", ] ``` **Step 5: 提交** ```bash git add backend/src/schemas/ git commit -m "[backend] feat: 添加Pydantic Schema模型" ``` --- ## Task 5: 创建认证中间件和依赖注入 **Files:** - Create: `backend/src/middleware/__init__.py` - Create: `backend/src/middleware/auth.py` - Create: `backend/src/dependencies.py` **Step 1: 创建认证中间件** ```python # backend/src/middleware/auth.py from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from config.database import get_db from src.models.user import User from src.utils.jwt import decode_access_token security = HTTPBearer() async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db), ) -> User: """获取当前用户""" token = credentials.credentials # 解码token payload = decode_access_token(token) if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token无效或过期", headers={"WWW-Authenticate": "Bearer"}, ) user_id = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token无效", ) # 查询用户 result = await db.execute(select(User).where(User.id == int(user_id))) user = result.scalar_one_or_none() if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在", ) if not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户已被禁用", ) return user async def get_current_active_user( current_user: User = Depends(get_current_user), ) -> User: """获取当前激活用户""" if not current_user.is_active: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户未激活", ) return current_user async def require_admin( current_user: User = Depends(get_current_active_user), ) -> User: """要求管理员权限""" if current_user.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="需要管理员权限", ) return current_user async def require_market_or_admin( current_user: User = Depends(get_current_active_user), ) -> User: """要求市场部或管理员权限""" if current_user.role not in ["admin", "market"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="需要市场部或管理员权限", ) return current_user ``` **Step 2: 创建依赖注入模块** ```python # backend/src/dependencies.py from fastapi import Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from config.database import get_db from src.models.user import User from src.models.project import Project from .middleware.auth import get_current_user async def get_project_or_404( project_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> Project: """获取项目或返回404""" result = await db.execute(select(Project).where(Project.id == project_id)) project = result.scalar_one_or_none() if project is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在", ) return project async def get_project_if_owner_or_admin( project_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> Project: """获取项目(仅限创建者或管理员)""" project = await get_project_or_404(project_id, current_user, db) if current_user.role != "admin" and project.created_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此项目", ) return project async def get_user_or_404( user_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> User: """获取用户或返回404""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在", ) return user ``` **Step 3: 创建中间件包初始化文件** ```python # backend/src/middleware/__init__.py from .auth import ( get_current_user, get_current_active_user, require_admin, require_market_or_admin, ) __all__ = [ "get_current_user", "get_current_active_user", "require_admin", "require_market_or_admin", ] ``` **Step 4: 提交** ```bash git add backend/src/middleware/ backend/src/dependencies.py git commit -m "[backend] feat: 添加认证中间件和依赖注入" ``` --- ## Task 6: 创建认证路由 **Files:** - Create: `backend/src/routes/__init__.py` - Create: `backend/src/routes/auth.py` **Step 1: 创建认证路由** ```python # backend/src/routes/auth.py from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from config.database import get_db from src.models.user import User from src.schemas.auth import LoginRequest, LoginResponse from src.schemas.user import UserResponse from src.utils.password import verify_password from src.utils.jwt import create_access_token from src.middleware.auth import get_current_user from datetime import timedelta router = APIRouter(prefix="/auth", tags=["认证"]) @router.post("/login", response_model=dict) async def login( login_data: LoginRequest, db: AsyncSession = Depends(get_db), ): """用户登录""" # 查询用户 result = await db.execute( select(User).where(User.username == login_data.username) ) user = result.scalar_one_or_none() # 验证用户和密码 if not user or not verify_password(login_data.password, user.password_hash): return { "success": False, "message": "用户名或密码错误", "data": None, "error_code": "1002", } if not user.is_active: return { "success": False, "message": "用户未激活", "data": None, "error_code": "1002", } # 生成token access_token = create_access_token( data={"sub": str(user.id), "username": user.username} ) return { "success": True, "message": "登录成功", "data": { "token": access_token, "token_type": "bearer", "user": UserResponse.model_validate(user), }, "error_code": None, } @router.get("/me", response_model=dict) async def get_current_user_info( current_user: User = Depends(get_current_user), ): """获取当前用户信息""" return { "success": True, "message": "获取成功", "data": UserResponse.model_validate(current_user), "error_code": None, } @router.post("/logout", response_model=dict) async def logout(current_user: User = Depends(get_current_user)): """用户登出""" return { "success": True, "message": "登出成功", "data": None, "error_code": None, } ``` **Step 2: 创建路由包初始化文件** ```python # backend/src/routes/__init__.py from .auth import router as auth_router from .users import router as users_router from .projects import router as projects_router __all__ = [ "auth_router", "users_router", "projects_router", ] ``` **Step 3: 提交** ```bash git add backend/src/routes/auth.py git commit -m "[backend] feat: 添加认证路由" ``` --- ## Task 7: 创建用户管理路由 **Files:** - Create: `backend/src/routes/users.py` **Step 1: 创建用户管理路由** ```python # backend/src/routes/users.py from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, or_ from config.database import get_db from src.models.user import User from src.schemas.user import ( UserCreate, UserUpdate, UserResponse, UserListResponse, PasswordReset, ) from src.utils.password import hash_password from src.middleware.auth import require_admin from typing import Optional router = APIRouter(prefix="/users", tags=["用户管理"]) @router.get("", response_model=dict) async def get_users( page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), department: Optional[str] = None, role: Optional[str] = None, keyword: Optional[str] = None, current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """获取用户列表""" # 构建查询 query = select(User) # 筛选条件 if department: query = query.where(User.department == department) if role: query = query.where(User.role == role) if keyword: query = query.where( or_( User.username.contains(keyword), User.real_name.contains(keyword), User.email.contains(keyword), ) ) # 总数 total_query = select(func.count()).select_from(query.subquery()) total_result = await db.execute(total_query) total = total_result.scalar() # 分页 query = query.offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) users = result.scalars().all() return { "success": True, "message": "获取成功", "data": { "items": [UserResponse.model_validate(u) for u in users], "total": total, "page": page, "page_size": page_size, }, "error_code": None, } @router.post("", response_model=dict) async def create_user( user_data: UserCreate, current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """创建用户""" # 检查用户名是否已存在 result = await db.execute( select(User).where(User.username == user_data.username) ) if result.scalar_one_or_none(): return { "success": False, "message": "用户名已存在", "data": None, "error_code": "2002", } # 检查邮箱是否已存在 if user_data.email: result = await db.execute( select(User).where(User.email == user_data.email) ) if result.scalar_one_or_none(): return { "success": False, "message": "邮箱已存在", "data": None, "error_code": "2002", } # 创建用户 user = User( username=user_data.username, password_hash=hash_password(user_data.password), real_name=user_data.real_name, department=user_data.department, role=user_data.role, email=user_data.email, phone=user_data.phone, ) db.add(user) await db.commit() await db.refresh(user) return { "success": True, "message": "用户创建成功", "data": UserResponse.model_validate(user), "error_code": None, } @router.get("/{user_id}", response_model=dict) async def get_user( user_id: int, current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """获取用户详情""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return { "success": False, "message": "用户不存在", "data": None, "error_code": "2001", } return { "success": True, "message": "获取成功", "data": UserResponse.model_validate(user), "error_code": None, } @router.put("/{user_id}", response_model=dict) async def update_user( user_id: int, user_data: UserUpdate, current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """更新用户""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return { "success": False, "message": "用户不存在", "data": None, "error_code": "2001", } # 更新字段 update_data = user_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(user, field, value) await db.commit() await db.refresh(user) return { "success": True, "message": "用户更新成功", "data": UserResponse.model_validate(user), "error_code": None, } @router.delete("/{user_id}", response_model=dict) async def delete_user( user_id: int, current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """删除用户""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return { "success": False, "message": "用户不存在", "data": None, "error_code": "2001", } await db.delete(user) await db.commit() return { "success": True, "message": "用户删除成功", "data": None, "error_code": None, } @router.post("/{user_id}/reset-password", response_model=dict) async def reset_password( user_id: int, password_data: PasswordReset, current_user: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """重置用户密码""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: return { "success": False, "message": "用户不存在", "data": None, "error_code": "2001", } user.password_hash = hash_password(password_data.new_password) await db.commit() return { "success": True, "message": "密码重置成功", "data": None, "error_code": None, } ``` **Step 2: 提交** ```bash git add backend/src/routes/users.py git commit -m "[backend] feat: 添加用户管理路由" ``` --- ## Task 8: 创建项目管理路由 **Files:** - Create: `backend/src/routes/projects.py` **Step 1: 创建项目管理路由** ```python # backend/src/routes/projects.py from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from config.database import get_db from src.models.user import User from src.models.project import Project from src.schemas.project import ( ProjectCreate, ProjectUpdate, ProjectResponse, ProjectListResponse, ) from src.middleware.auth import ( get_current_user, require_market_or_admin, ) from src.dependencies import get_project_or_404 from typing import Optional from datetime import date from decimal import Decimal router = APIRouter(prefix="/projects", tags=["项目管理"]) @router.get("", response_model=dict) async def get_projects( page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), project_no: Optional[str] = None, engineering_type: Optional[str] = None, project_department: Optional[str] = None, signing_date_start: Optional[date] = None, signing_date_end: Optional[date] = None, contract_amount_min: Optional[Decimal] = None, contract_amount_max: Optional[Decimal] = None, keyword: Optional[str] = None, sort_by: Optional[str] = None, sort_order: Optional[str] = Query(None, pattern="^(asc|desc)$"), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取项目列表""" # 构建查询 query = select(Project) # 筛选条件 conditions = [] if project_no: conditions.append(Project.project_no == project_no) if engineering_type: conditions.append(Project.engineering_type == engineering_type) if project_department: conditions.append(Project.project_department == project_department) if signing_date_start: conditions.append(Project.signing_date >= signing_date_start) if signing_date_end: conditions.append(Project.signing_date <= signing_date_end) if contract_amount_min: conditions.append(Project.contract_amount >= contract_amount_min) if contract_amount_max: conditions.append(Project.contract_amount <= contract_amount_max) if keyword: conditions.append( (Project.name.contains(keyword)) | (Project.owner_unit.contains(keyword)) ) if conditions: query = query.where(and_(*conditions)) # 排序 if sort_by: sort_column = getattr(Project, sort_by, None) if sort_column: order = desc if sort_order == "desc" else asc query = query.order_by(order(sort_column)) else: query = query.order_by(Project.created_at.desc()) # 总数 total_query = select(func.count()).select_from(query.subquery()) total_result = await db.execute(total_query) total = total_result.scalar() # 分页 query = query.offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) projects = result.scalars().all() # 获取创建人姓名 project_data = [] for p in projects: creator_result = await db.execute( select(User.real_name).where(User.id == p.created_by) ) creator_name = creator_result.scalar_one_or_none() p_dict = ProjectResponse.model_validate(p).model_dump() p_dict["created_by_name"] = creator_name project_data.append(p_dict) return { "success": True, "message": "获取成功", "data": { "items": project_data, "total": total, "page": page, "page_size": page_size, }, "error_code": None, } @router.get("/{project_id}", response_model=dict) async def get_project( project: Project = Depends(get_project_or_404), current_user: User = Depends(get_current_user), ): """获取项目详情""" from sqlalchemy import select result = await current_user.execute( select(User.real_name).where(User.id == project.created_by) ) creator_name = result.scalar_one_or_none() project_dict = ProjectResponse.model_validate(project).model_dump() project_dict["created_by_name"] = creator_name return { "success": True, "message": "获取成功", "data": project_dict, "error_code": None, } @router.post("", response_model=dict) async def create_project( project_data: ProjectCreate, current_user: User = Depends(require_market_or_admin), db: AsyncSession = Depends(get_db), ): """创建项目""" # 检查项目编号是否已存在 result = await db.execute( select(Project).where(Project.project_no == project_data.project_no) ) if result.scalar_one_or_none(): return { "success": False, "message": "项目编号已存在", "data": None, "error_code": "2002", } # 创建项目 project = Project( **project_data.model_dump(), created_by=current_user.id, ) db.add(project) await db.commit() await db.refresh(project) return { "success": True, "message": "项目创建成功", "data": ProjectResponse.model_validate(project), "error_code": None, } @router.put("/{project_id}", response_model=dict) async def update_project( project_id: int, project_data: ProjectUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """更新项目""" # 获取项目 result = await db.execute(select(Project).where(Project.id == project_id)) project = result.scalar_one_or_none() if not project: return { "success": False, "message": "项目不存在", "data": None, "error_code": "2001", } # 权限检查 if current_user.role == "market" and project.created_by != current_user.id: return { "success": False, "message": "无权修改此项目", "data": None, "error_code": "3001", } # 更新字段 update_data = project_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(project, field, value) await db.commit() await db.refresh(project) return { "success": True, "message": "项目更新成功", "data": ProjectResponse.model_validate(project), "error_code": None, } @router.delete("/{project_id}", response_model=dict) async def delete_project( project_id: int, current_user: User = Depends(require_market_or_admin), db: AsyncSession = Depends(get_db), ): """删除项目""" result = await db.execute(select(Project).where(Project.id == project_id)) project = result.scalar_one_or_none() if not project: return { "success": False, "message": "项目不存在", "data": None, "error_code": "2001", } # 权限检查 if current_user.role == "market" and project.created_by != current_user.id: return { "success": False, "message": "无权删除此项目", "data": None, "error_code": "3001", } await db.delete(project) await db.commit() return { "success": True, "message": "项目删除成功", "data": None, "error_code": None, } @router.get("/statistics/basic", response_model=dict) async def get_statistics( engineering_type: Optional[str] = None, signing_date_start: Optional[date] = None, signing_date_end: Optional[date] = None, contract_amount_min: Optional[Decimal] = None, contract_amount_max: Optional[Decimal] = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取基础统计""" # 构建查询 query = select(Project) conditions = [] if engineering_type: conditions.append(Project.engineering_type == engineering_type) if signing_date_start: conditions.append(Project.signing_date >= signing_date_start) if signing_date_end: conditions.append(Project.signing_date <= signing_date_end) if contract_amount_min: conditions.append(Project.contract_amount >= contract_amount_min) if contract_amount_max: conditions.append(Project.contract_amount <= contract_amount_max) if conditions: query = query.where(and_(*conditions)) result = await db.execute(query) projects = result.scalars().all() # 统计 total_count = len(projects) total_contract_amount = sum( p.contract_amount or Decimal(0) for p in projects ) total_receipt_amount = sum( p.actual_receipt_amount or Decimal(0) for p in projects ) total_payment_amount = sum( p.actual_payment_amount or Decimal(0) for p in projects ) avg_receipt_rate = 0 if total_contract_amount > 0: avg_receipt_rate = ( (total_receipt_amount / total_contract_amount) * 100 ) avg_payment_rate = 0 total_payable = sum(p.payable_amount or Decimal(0) for p in projects) if total_payable > 0: avg_payment_rate = ( (total_payment_amount / total_payable) * 100 ) avg_progress = 0 if total_count > 0: avg_progress = sum( p.cumulative_progress or Decimal(0) for p in projects ) / total_count return { "success": True, "message": "统计成功", "data": { "total_count": total_count, "total_contract_amount": float(total_contract_amount), "total_receipt_amount": float(total_receipt_amount), "total_payment_amount": float(total_payment_amount), "avg_receipt_completion_rate": float(avg_receipt_rate), "avg_payment_completion_rate": float(avg_payment_rate), "avg_cumulative_progress": float(avg_progress), }, "error_code": None, } @router.get("/statistics/group", response_model=dict) async def get_group_statistics( group_by: str = Query(..., pattern="^(engineering_type|project_department)$"), signing_date_start: Optional[date] = None, signing_date_end: Optional[date] = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取分组统计""" # 构建查询 query = select(Project) if signing_date_start: query = query.where(Project.signing_date >= signing_date_start) if signing_date_end: query = query.where(Project.signing_date <= signing_date_end) result = await db.execute(query) projects = result.scalars().all() # 分组统计 groups = {} for p in projects: group_value = getattr(p, group_by) if group_value not in groups: groups[group_value] = { "count": 0, "total_contract_amount": Decimal(0), "total_receipt_amount": Decimal(0), "total_payment_amount": Decimal(0), } groups[group_value]["count"] += 1 groups[group_value]["total_contract_amount"] += p.contract_amount or Decimal(0) groups[group_value]["total_receipt_amount"] += p.actual_receipt_amount or Decimal(0) groups[group_value]["total_payment_amount"] += p.actual_payment_amount or Decimal(0) # 转换为列表 result_list = [] for key, value in groups.items(): if group_by == "engineering_type": item = {"engineering_type": key} else: item = {"project_department": key} item.update({ "count": value["count"], "total_contract_amount": float(value["total_contract_amount"]), "total_receipt_amount": float(value["total_receipt_amount"]), "total_payment_amount": float(value["total_payment_amount"]), }) result_list.append(item) return { "success": True, "message": "统计成功", "data": result_list, "error_code": None, } @router.get("/statistics/timeline", response_model=dict) async def get_timeline_statistics( time_field: str = Query(..., pattern="^(signing_date|start_date|planned_end_date)$"), group_by: str = Query("month", pattern="^(day|month|year)$"), engineering_type: Optional[str] = None, start_date: Optional[date] = None, end_date: Optional[date] = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取时间维度统计""" # 构建查询 query = select(Project) if engineering_type: query = query.where(Project.engineering_type == engineering_type) if start_date: date_column = getattr(Project, time_field) query = query.where(date_column >= start_date) if end_date: date_column = getattr(Project, time_field) query = query.where(date_column <= end_date) result = await db.execute(query) projects = result.scalars().all() # 时间分组 groups = {} for p in projects: date_value = getattr(p, time_field) if not date_value: continue if group_by == "day": key = date_value.strftime("%Y-%m-%d") elif group_by == "month": key = date_value.strftime("%Y-%m") else: # year key = str(date_value.year) if key not in groups: groups[key] = {"count": 0, "total_contract_amount": Decimal(0)} groups[key]["count"] += 1 groups[key]["total_contract_amount"] += p.contract_amount or Decimal(0) # 转换为列表并排序 result_list = [] for key in sorted(groups.keys()): value = groups[key] if group_by == "day": item = {"day": key} elif group_by == "month": item = {"month": key} else: item = {"year": key} item.update({ "count": value["count"], "total_contract_amount": float(value["total_contract_amount"]), }) result_list.append(item) return { "success": True, "message": "统计成功", "data": result_list, "error_code": None, } ``` **Step 2: 提交** ```bash git add backend/src/routes/projects.py git commit -m "[backend] feat: 添加项目管理路由" ``` --- ## Task 9: 创建FastAPI主应用 **Files:** - Create: `backend/main.py` **Step 1: 创建主应用** ```python # backend/main.py from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from config.settings import get_settings from config.database import init_db from src.routes import auth_router, users_router, projects_router settings = get_settings() # 创建FastAPI应用 app = FastAPI( title=settings.APP_NAME, version=settings.APP_VERSION, description="海洋项目管理系统后端API", docs_url="/docs", redoc_url="/redoc", ) # 配置CORS app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 注册路由 app.include_router(auth_router, prefix="/api/v1") app.include_router(users_router, prefix="/api/v1") app.include_router(projects_router, prefix="/api/v1") @app.on_event("startup") async def startup_event(): """应用启动事件""" await init_db() @app.get("/") async def root(): """根路径""" return { "app": settings.APP_NAME, "version": settings.APP_VERSION, "status": "running", } @app.get("/health") async def health(): """健康检查""" return {"status": "healthy"} if __name__ == "__main__": import uvicorn uvicorn.run( "main:app", host="0.0.0.0", port=5000, reload=settings.DEBUG, ) ``` **Step 2: 提交** ```bash git add backend/main.py git commit -m "[backend] feat: 创建FastAPI主应用" ``` --- ## Task 10: 创建环境配置文件 **Files:** - Create: `backend/.env.example` - Create: `backend/.env` **Step 1: 创建环境变量示例文件** ```env # backend/.env.example # 应用配置 APP_NAME=海洋项目管理系统 APP_VERSION=1.0.0 DEBUG=True # 数据库配置 DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=rootpassword DB_NAME=project_manager DB_CHARSET=utf8mb4 # JWT配置 SECRET_KEY=your-secret-key-change-this-in-production ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=1440 # CORS配置 CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 ``` **Step 2: 创建环境变量文件** ```env # backend/.env APP_NAME=海洋项目管理系统 APP_VERSION=1.0.0 DEBUG=True DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=rootpassword DB_NAME=project_manager DB_CHARSET=utf8mb4 SECRET_KEY=ocean-project-manager-secret-key-2026 ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=1440 CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 ``` **Step 3: 创建.gitignore** ``` # backend/.gitignore __pycache__/ *.py[cod] *$py.class *.so .Python env/ venv/ ENV/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg .env .pytest_cache/ .coverage htmlcov/ *.cover .hypothesis/ .mypy_cache/ .dmypy.json dmypy.json ``` **Step 4: 提交** ```bash git add backend/.env.example backend/.env backend/.gitignore git commit -m "[backend] feat: 添加环境配置文件" ``` --- ## Task 11: 初始化数据库并创建测试用户 **Step 1: 启动MySQL服务** Run: `sudo systemctl start mysql` (如果MySQL未运行) Expected: MySQL服务启动成功 **Step 2: 创建数据库** Run: `mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS project_manager DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;"` Expected: 数据库创建成功 **Step 3: 执行初始化脚本** Run: `mysql -u root -p project_manager < backend/config/init-database.sql` Expected: 表结构和初始数据创建成功 **Step 4: 验证数据库** Run: `mysql -u root -p project_manager -e "SHOW TABLES;"` Expected: 显示users和projects表 --- ## Task 12: 运行测试并修复问题 **Step 1: 运行所有测试** Run: `cd backend && pytest tests/ -v` Expected: 测试运行 **Step 2: 查看测试结果** Run: `cd backend && pytest tests/ --cov=src --cov-report=term-missing` Expected: 生成覆盖率报告 **Step 3: 修复失败的测试** 根据测试输出,修复代码中的问题 **Step 4: 重新运行测试** Run: `cd backend && pytest tests/ -v` Expected: 所有测试通过 **Step 5: 提交** ```bash git add backend/ git commit -m "[backend] test: 修复测试问题,所有测试通过" ``` --- ## Task 13: 启动应用并验证API **Step 1: 启动FastAPI应用** Run: `cd backend && python main.py` Expected: 应用启动在 http://0.0.0.0:5000 **Step 2: 访问Swagger文档** Run: 在浏览器中打开 `http://localhost:5000/docs` Expected: 显示API文档 **Step 3: 测试登录接口** Run: `curl -X POST http://localhost:5000/api/v1/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}'` Expected: 返回token和用户信息 **Step 4: 测试获取用户信息** Run: 使用返回的token ``` curl http://localhost:5000/api/v1/auth/me -H "Authorization: Bearer " ``` Expected: 返回用户信息 **Step 5: 提交** ```bash git commit --allow-empty -m "[backend] feat: 后端开发完成,所有功能正常" ``` --- ## Task 14: 更新文档 **Files:** - Modify: `backend/README.md` **Step 1: 更新README** ```markdown # 海洋项目管理系统 - 后端 ## 快速开始 ### 1. 安装依赖 ```bash pip install -r requirements.txt -r requirements-test.txt ``` ### 2. 配置环境变量 复制 `.env.example` 为 `.env` 并修改配置 ```bash cp .env.example .env ``` ### 3. 初始化数据库 ```bash mysql -u root -p < config/init-database.sql ``` ### 4. 启动应用 ```bash python main.py ``` 应用将在 http://localhost:5000 启动 ### 5. 访问API文档 Swagger UI: http://localhost:5000/docs ReDoc: http://localhost:5000/redoc ## 运行测试 ```bash pytest tests/ -v ``` 生成覆盖率报告: ```bash pytest tests/ --cov=src --cov-report=html ``` ## API文档 详细的API文档请参考: `docs/api.md` ## 开发规范 详见: `WORKSTANDARDS.md` ``` **Step 2: 提交** ```bash git add backend/README.md git commit -m "[backend] docs: 更新README文档" ``` --- ## 验证检查清单 在完成所有任务后,验证以下内容: - [ ] 所有依赖安装成功 - [ ] 数据库表创建成功 - [ ] 应用可以正常启动 - [ ] Swagger文档可以访问 - [ ] 登录接口测试通过 - [ ] 所有测试用例通过(51个测试) - [ ] 测试覆盖率 > 80% - [ ] 文档更新完成