Files
dpb/backend/app/api/v1/module_bre/alert/job.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

151 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""超期预警每日扫描 job(批3):树多年未决策 / 花粉批次过期 / 种子批超期。
作为系统级定时任务注册于 app/core/ap_scheduler.pyinit_scheduler),
每日执行一次。生成 bre_alert 行时直接 db.add(不经 CRUDBase,防自审计递归);
同实体已有 open 预警时跳过(去重),待处理人 resolve 后下一轮可再次告警。
"""
import logging
from datetime import date, datetime, timedelta
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import async_db_session
from .model import AlertModel
from app.api.v1.module_bre.tree.model import TreeModel
from app.api.v1.module_bre.selection_result.model import SelectionResultModel
from app.api.v1.module_bre.pollen.model import PollenModel
from app.api.v1.module_bre.seed_lot.model import SeedLotModel
logger = logging.getLogger(__name__)
# 桃育种:杂种实生苗童期通常 3 年进入初选,定植后 3 年仍无选育决策即预警
TREE_UNDECIDED_YEARS = 3
# 种子批库存超过 harvest_year 的年限视为超期,需复检活力/更新
SEED_LOT_MAX_YEARS = 3
def _cutoff_iso(years: int, today: date) -> str:
"""距今 years 年整天的下一日(含当日时间戳单株也命中)。"""
boundary = datetime(today.year - years, today.month, today.day) + timedelta(days=1)
return boundary.date().isoformat()
async def _add_open_alert(
db: AsyncSession,
alert_type: str,
entity_type: str,
entity_id: int,
message: str,
) -> None:
"""落一条 open 预警(调用方已确认无同实体 open 记录)。"""
db.add(
AlertModel(
alert_type=alert_type,
entity_type=entity_type,
entity_id=entity_id,
message=message,
status="open",
)
)
async def run_overdue_scan() -> bool:
"""每日超期预警扫描:写 bre_alert(open 去重)。返回本轮新增条数。"""
today = date.today()
written = 0
async with async_db_session() as db:
# ① 树 N 年未决策:定植距今超 N 年且无任何 selection_result 记录
open_tree = (
select(AlertModel.id)
.where(
AlertModel.alert_type == "tree_undecided",
AlertModel.entity_type == "bre_tree",
AlertModel.entity_id == TreeModel.id,
AlertModel.status == "open",
)
.exists()
)
trees = (await db.execute(
select(TreeModel).where(
TreeModel.planted_date.isnot(None),
TreeModel.planted_date < _cutoff_iso(TREE_UNDECIDED_YEARS, today),
~select(SelectionResultModel.id)
.where(SelectionResultModel.tree_id == TreeModel.id)
.exists(),
~open_tree,
)
)).scalars().all()
for t in trees:
await _add_open_alert(
db,
"tree_undecided",
"bre_tree",
t.id,
f"单株 {t.tree_no} 定植于 {t.planted_date},已超 {TREE_UNDECIDED_YEARS} 年无选育决策(selection_result 无记录),请及时评定",
)
written += 1
# ② 花粉批次过期:expiry_date 早于今天
open_pollen = (
select(AlertModel.id)
.where(
AlertModel.alert_type == "pollen_expiry",
AlertModel.entity_type == "bre_pollen",
AlertModel.entity_id == PollenModel.id,
AlertModel.status == "open",
)
.exists()
)
pollens = (await db.execute(
select(PollenModel).where(
PollenModel.expiry_date.isnot(None),
PollenModel.expiry_date < today,
~open_pollen,
)
)).scalars().all()
for p in pollens:
await _add_open_alert(
db,
"pollen_expiry",
"bre_pollen",
p.id,
f"花粉批次 {p.lot_code} 已于 {p.expiry_date} 失效(贮藏 {p.storage_method}),请复测活力或重新采集",
)
written += 1
# ③ 种子批超期:harvest_year 距今超 N 年(按收获年份)
open_lot = (
select(AlertModel.id)
.where(
AlertModel.alert_type == "seed_lot_expiry",
AlertModel.entity_type == "bre_seed_lot",
AlertModel.entity_id == SeedLotModel.id,
AlertModel.status == "open",
)
.exists()
)
lots = (await db.execute(
select(SeedLotModel).where(
SeedLotModel.harvest_year.isnot(None),
SeedLotModel.harvest_year <= today.year - SEED_LOT_MAX_YEARS,
~open_lot,
)
)).scalars().all()
for lot in lots:
age = today.year - (lot.harvest_year or today.year)
await _add_open_alert(
db,
"seed_lot_expiry",
"bre_seed_lot",
lot.id,
f"种子批 {lot.lot_code}{lot.harvest_year} 年收获,已存放 {age} 年,建议复检发芽率/更新库存",
)
written += 1
await db.commit()
logger.info(f"超期预警扫描完成:新增 {written} 条预警")
return written