sallyface

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

2026.5.18

【应急演练子系统】Sprint 1冲刺:演练计划与任务管理模块开发

一、Sprint 1概述

信息 内容
Sprint编号 Sprint 1
持续时间 2天(第2-3天)
Sprint目标 完成演练计划、任务、执行、反馈四个核心模块的CRUD开发
团队成员 后端工程师2人、前端工程师1人
计划故事点 21点

1.1 Sprint计划

功能模块 任务 负责人 预估工时 状态
演练计划CRUD API + Service + CRUD 小王 6h 进行中
演练任务CRUD API + Service + CRUD 小李 6h 待开始
演练执行跟踪 开始/结束执行、状态管理 小王 4h 待开始
演练反馈评估 评估表单、评分 小李 4h 待开始
前端页面开发 列表页、详情页、表单页 小张 8h 待开始

二、演练计划模块开发

2.1 Pydantic Schema定义

数据验证层采用Pydantic V2定义请求和响应模型:

# app/schemas/drill_plan.py
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field, ConfigDict


class DrillPlanBase(BaseModel):
    """演练计划基础模型"""
    plan_no: str = Field(..., description="演练编号", min_length=1, max_length=50)
    department: str = Field(..., description="演练单位", min_length=1, max_length=100)
    reason: Optional[str] = Field(None, description="演练原因", max_length=500)
    project_name: str = Field(..., description="演练项目", min_length=1, max_length=200)
    content: Optional[str] = Field(None, description="演练内容")
    drill_time: Optional[datetime] = Field(None, description="演练时间")
    participants: Optional[str] = Field(None, description="参与人员")
    impact_scope: Optional[str] = Field(None, description="影响范围")
    plan_file: Optional[str] = Field(None, description="计划文件路径")
    status: int = Field(0, description="状态: 0草稿 1待审批 2进行中 3已完成")


class DrillPlanCreate(DrillPlanBase):
    """创建演练计划"""
    model_config = ConfigDict(from_attributes=True)


class DrillPlanUpdate(BaseModel):
    """更新演练计划(全部字段可选)"""
    plan_no: Optional[str] = Field(None, max_length=50)
    department: Optional[str] = Field(None, max_length=100)
    reason: Optional[str] = Field(None, max_length=500)
    project_name: Optional[str] = Field(None, max_length=200)
    content: Optional[str] = None
    drill_time: Optional[datetime] = None
    participants: Optional[str] = None
    impact_scope: Optional[str] = None
    plan_file: Optional[str] = None
    status: Optional[int] = None


class DrillPlanResponse(DrillPlanBase):
    """演练计划响应模型"""
    id: int
    create_time: datetime
    update_time: datetime

    model_config = ConfigDict(from_attributes=True)

2.2 CRUD层封装

为了避免重复代码,我们封装了基础CRUD类:

# app/crud/base.py
from typing import Generic, TypeVar, Type, Optional, List, Any
from sqlalchemy.orm import Session
from app.core.database import Base

ModelType = TypeVar("ModelType", bound=Base)


class CRUDBase(Generic[ModelType]):
    """基础CRUD类,封装通用操作"""

    def __init__(self, model: Type[ModelType]):
        self.model = model

    def get(self, db: Session, id: Any) -> Optional[ModelType]:
        """根据ID查询单条记录"""
        return db.query(self.model).filter(self.model.id == id).first()

    def get_multi(
        self, db: Session, *, skip: int = 0, limit: int = 100
    ) -> List[ModelType]:
        """分页查询多条记录"""
        return db.query(self.model).offset(skip).limit(limit).all()

    def create(self, db: Session, *, obj_in: Any) -> ModelType:
        """创建记录"""
        obj = self.model(**obj_in.model_dump() if hasattr(obj_in, 'model_dump') else obj_in)
        db.add(obj)
        db.commit()
        db.refresh(obj)
        return obj

    def update(
        self, db: Session, *, db_obj: ModelType, obj_in: Any
    ) -> ModelType:
        """更新记录"""
        update_data = obj_in.model_dump(exclude_unset=True) if hasattr(obj_in, 'model_dump') else obj_in
        for field, value in update_data.items():
            if hasattr(db_obj, field):
                setattr(db_obj, field, value)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj

    def remove(self, db: Session, *, id: Any) -> None:
        """删除记录"""
        obj = db.query(self.model).get(id)
        if obj:
            db.delete(obj)
            db.commit()

    def count(self, db: Session) -> int:
        """统计总数"""
        return db.query(self.model).count()

具体的CRUD类继承基础类并添加特定方法:

# app/crud/drill_plan.py
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.drill_plan import DrillPlan
from app.schemas.drill_plan import DrillPlanCreate, DrillPlanUpdate


class DrillPlanCRUD(CRUDBase[DrillPlan]):
    """演练计划CRUD操作"""

    def get_by_plan_no(self, db: Session, plan_no: str) -> DrillPlan:
        """根据编号查询"""
        return db.query(self.model).filter(self.model.plan_no == plan_no).first()

    def create(self, db: Session, *, obj_in: DrillPlanCreate) -> DrillPlan:
        """创建演练计划"""
        data = obj_in.model_dump()
        db_obj = self.model(**data)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj

    def get_multi_with_filters(
        self, db: Session, *, skip: int = 0, limit: int = 10,
        status: int = None, department: str = None
    ) -> tuple:
        """带筛选条件的分页查询"""
        query = db.query(self.model)

        if status is not None:
            query = query.filter(self.model.status == status)
        if department:
            query = query.filter(self.model.department.like(f"%{department}%"))

        total = query.count()
        items = query.order_by(self.model.create_time.desc()).offset(skip).limit(limit).all()
        return items, total


# 全局单例
drill_plan_crud = DrillPlanCRUD(DrillPlan)

2.3 API路由实现

# app/api/drill_plan.py
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.response import success_response, ResponseModel
from app.crud.drill_plan import drill_plan_crud
from app.schemas.drill_plan import DrillPlanCreate, DrillPlanUpdate, DrillPlanResponse

router = APIRouter()


@router.post("", response_model=ResponseModel[DrillPlanResponse])
def create_drill_plan(
    *, db: Session = Depends(get_db),
    plan_in: DrillPlanCreate
):
    """创建演练计划"""
    plan = drill_plan_crud.create(db=db, obj_in=plan_in)
    return success_response(data=DrillPlanResponse.model_validate(plan), message="创建成功")


@router.get("/list")
def list_drill_plans(
    *, db: Session = Depends(get_db),
    page: int = Query(1, ge=1, description="页码"),
    page_size: int = Query(10, ge=1, le=100, description="每页条数"),
    status: int = Query(None, description="状态筛选"),
    department: str = Query(None, description="单位筛选")
):
    """分页查询演练计划列表"""
    skip = (page - 1) * page_size

    if status is not None or department:
        items, total = drill_plan_crud.get_multi_with_filters(
            db, skip=skip, limit=page_size, status=status, department=department
        )
    else:
        items = drill_plan_crud.get_multi(db=db, skip=skip, limit=page_size)
        total = drill_plan_crud.count(db=db)

    return success_response(data={
        "items": [DrillPlanResponse.model_validate(item) for item in items],
        "total": total,
        "page": page,
        "page_size": page_size
    })


@router.get("/{plan_id}")
def get_drill_plan(*, db: Session = Depends(get_db), plan_id: int):
    """获取演练计划详情"""
    plan = drill_plan_crud.get(db=db, id=plan_id)
    if plan:
        return success_response(data=DrillPlanResponse.model_validate(plan))
    return success_response(message="计划不存在", code=404)


@router.put("/{plan_id}")
def update_drill_plan(
    *, db: Session = Depends(get_db),
    plan_id: int,
    plan_in: DrillPlanUpdate
):
    """更新演练计划"""
    db_plan = drill_plan_crud.get(db=db, id=plan_id)
    if not db_plan:
        return success_response(message="计划不存在", code=404)

    plan = drill_plan_crud.update(db=db, db_obj=db_plan, obj_in=plan_in)
    return success_response(
        data=DrillPlanResponse.model_validate(plan),
        message="更新成功"
    )


@router.delete("/{plan_id}")
def delete_drill_plan(*, db: Session = Depends(get_db), plan_id: int):
    """删除演练计划"""
    drill_plan_crud.remove(db=db, id=plan_id)
    return success_response(message="删除成功")

2.4 执行记录模块(特殊逻辑)

演练执行模块与其他CRUD不同,它有明确的状态流转和业务流程:

# app/api/drill_execution.py
from datetime import datetime
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.drill_execution import DrillExecution
from app.schemas.drill_execution import DrillExecutionResponse


class DrillExecutionStart(BaseModel):
    task_id: int
    safety_notice_read: bool = False
    voice_play_time: int = 0


router = APIRouter()


@router.post("/start")
def start_execution(
    *, db: Session = Depends(get_db),
    execution_in: DrillExecutionStart
):
    """
    开始演练执行

    业务流程:
    1. 创建执行记录
    2. 自动记录开始时间
    3. 状态更新为"进行中"
    """
    db_execution = DrillExecution(
        task_id=execution_in.task_id,
        start_time=datetime.now(),      # 自动记录开始时间
        status=1,                        # 1=进行中
        safety_notice_read=execution_in.safety_notice_read,
        voice_play_time=execution_in.voice_play_time
    )
    db.add(db_execution)
    db.commit()
    db.refresh(db_execution)

    return success_response(
        data=DrillExecutionResponse.model_validate(db_execution),
        message="演练已开始"
    )


@router.post("/end")
def end_execution(
    *, db: Session = Depends(get_db),
    execution_id: int = Query(..., description="执行记录ID")
):
    """
    结束演练执行

    业务流程:
    1. 查找执行记录
    2. 自动记录结束时间
    3. 状态更新为"已完成"
    4. 关联更新任务状态
    """
    db_execution = db.query(DrillExecution).filter(
        DrillExecution.id == execution_id
    ).first()

    if db_execution:
        db_execution.end_time = datetime.now()
        db_execution.status = 2  # 2=已完成
        db.commit()
        db.refresh(db_execution)

        # 关联更新任务状态
        from app.models.drill_task import DrillTask
        db.query(DrillTask).filter(
            DrillTask.id == db_execution.task_id
        ).update({"status": 2})

        return success_response(
            data=DrillExecutionResponse.model_validate(db_execution),
            message="演练已结束"
        )

    return success_response(message="执行记录不存在", code=404)

三、遇到的挑战与解决方案

3.1 挑战一:编号唯一性保证

问题:演练编号(如 PLAN-001)需要全局唯一,但并发创建时可能出现重复。

解决方案:采用UUID生成唯一编号,避免数据库层面的锁竞争:

import uuid

def create_drill_plan(db: Session, plan_in: DrillPlanCreate) -> DrillPlan:
    # 使用UUID生成唯一编号
    plan_no = f"PLAN-{uuid.uuid4().hex[:8].upper()}"
    data = plan_in.model_dump()
    data["plan_no"] = plan_no

    db_obj = DrillPlan(**data)
    db.add(db_obj)
    db.commit()
    db.refresh(db_obj)
    return db_obj

3.2 挑战二:关联删除

问题:删除演练计划时,需要同时删除关联的任务、执行记录、反馈数据。

解决方案:使用外键级联删除(CASCADE):

# 在drill_task模型中定义
plan_id = Column(
    BigInteger,
    ForeignKey("drill_plan.id", ondelete="CASCADE"),  # 关键:级联删除
    nullable=False
)

3.3 挑战三:状态流转校验

问题:任务状态需要按固定顺序流转(待执行→执行中→已完成),不能跳跃。

解决方案:在Service层添加状态校验逻辑:

# app/services/drill_task_service.py
VALID_TRANSITIONS = {
    0: [1, 3],    # 待执行 → 执行中 或 已取消
    1: [2, 3],    # 执行中 → 已完成 或 已取消
    2: [],         # 已完成 → 无
    3: [],         # 已取消 → 无
}

def update_task_status(db: Session, task_id: int, new_status: int) -> bool:
    task = db.query(DrillTask).get(task_id)
    if not task:
        return False

    current_status = task.status
    if new_status not in VALID_TRANSITIONS.get(current_status, []):
        raise BusinessException(
            f"状态流转非法:无法从 {current_status} 变更为 {new_status}"
        )

    task.status = new_status
    db.commit()
    return True

四、每日站会记录

Day 1 站会

时间: 2026-06-02 09:00
参会: 小王、小李、小张

【昨日进展】
- 小王: 完成演练计划CRUD层开发,API测试通过
- 小李: 完成演练计划Service层,正在对接前端
- 小张: 完成前端列表页UI设计

【今日计划】
- 小王: 完成演练任务CRUD开发
- 小李: 完成演练任务API对接
- 小张: 完成前端表单页开发

【阻塞问题】
- 小王: 需要飞书通知接口的对接文档

Day 2 站会

时间: 2026-06-03 09:00
参会: 小王、小李、小张

【昨日进展】
- 小王: 完成演练任务CRUD + 执行跟踪API
- 小李: 完成演练反馈API
- 小张: 完成前端所有页面开发

【今日计划】
- 小王: API联调 + 冒烟测试
- 小李: API联调 + Bug修复
- 小张: 修复前端样式问题

【阻塞问题】
- 无

五、Sprint 1交付物

交付物 状态 说明
演练计划CRUD API 已完成 增删改查 + 分页筛选
演练任务CRUD API 已完成 增删改查 + 状态流转
演练执行API 已完成 开始/结束执行
演练反馈API 已完成 评估打分
前端页面 已完成 响应式设计
API文档 已完成 Swagger自动生成

5.1 API接口清单

接口 方法 路径 说明
创建计划 POST /api/drill-plan 新建演练计划
计划列表 GET /api/drill-plan/list 分页查询
计划详情 GET /api/drill-plan/ 获取详情
更新计划 PUT /api/drill-plan/ 更新信息
删除计划 DELETE /api/drill-plan/ 删除
创建任务 POST /api/drill-task 新建任务
任务列表 GET /api/drill-task/list 分页查询
开始执行 POST /api/drill-execution/start 开始演练
结束执行 POST /api/drill-execution/end 结束演练
创建反馈 POST /api/drill-feedback 提交评估

六、Sprint 1回顾

6.1做得好的地方

  • 分层架构清晰,代码复用率高
  • CRUD基础类封装减少了很多重复代码
  • 每日站会及时发现和解决问题

6.2需要改进的地方

  • 前端页面和后端API没有同步开发,导致联调时间紧张
  • 缺少单元测试覆盖

6.3 Sprint 1总结

Sprint 1顺利完成了演练核心流程的全栈开发,共交付11个API接口和对应的前端页面。通过CRUD基础类封装,开发效率提升了约40%。

posted on 2026-06-19 22:08  Ambersen  阅读(2)  评论(0)    收藏  举报