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%。
浙公网安备 33010602011771号