PMFlow/backend/app/api/sstc_api/signals_list.py

146 lines
6.5 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.

from __future__ import annotations
from .shared import *
from ...services.sstc.entity_lookup import _ensure_project
from ...services.sstc.feishu_routing import _backfill_feishu_sender_names
from ...services.sstc.signal_core import _as_dict, _extract_signal_attachments, _normalize_planner_status, _normalize_signal_status_code, _parse_project_id, _signal_ai_analysis, _signal_display_type, _signal_project_scope, _signal_source_context, _signal_status_label_from_code
from ...services.sstc.signal_filters import _build_signal_created_at_range, _extract_charter_related_signal_ids, _match_signal_filters, _normalize_signal_status_filter, _signal_proposer_name, _signal_title_text
router = APIRouter(tags=["sstc"])
@router.get("/projects/{project_id}/signals")
async def list_signals(
project_id: str,
status: Optional[str] = Query(default=None, description="pending/linked/closed/archived"),
keyword: Optional[str] = Query(default=None, description="按信号标题/内容/ID/提报人/关联宪章搜索"),
source: Optional[str] = Query(default=None, description="来源过滤"),
priority: Optional[str] = Query(default=None, description="优先级过滤"),
created_from: Optional[str] = Query(default=None, description="创建时间起始(北京时间 YYYY-MM-DD)"),
created_to: Optional[str] = Query(default=None, description="创建时间结束(北京时间 YYYY-MM-DD)"),
limit: int = Query(default=10, ge=1, le=100),
offset: int = Query(default=0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
pid = _parse_project_id(project_id)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
project = await _ensure_project(svc, pid)
project_creator_name = "-"
if project and getattr(project, "owner_id", None):
owner = (
await db.execute(
select(User.name).where(
and_(
User.tenant_id == tenant_id,
User.id == int(project.owner_id),
)
).limit(1)
)
).scalars().first()
if owner:
project_creator_name = str(owner)
signals = await svc.get_signals(pid)
await _backfill_feishu_sender_names(
db,
tenant_id=tenant_id,
signals=signals,
)
charters = await svc.get_charters(pid)
linked_signal_states: dict[int, str] = {}
linked_signal_charter_name: dict[int, str] = {}
for c in charters:
signal_ids = _extract_charter_related_signal_ids(c)
charter_status = (c.status or "").lower()
state = (
_SIGNAL_STATUS_CODE_CLOSED
if charter_status in {"aligned", "active", "online", "archived", "已对齐", "已上线", "已归档"}
else _SIGNAL_STATUS_CODE_LINKED
)
for sid in signal_ids:
if linked_signal_states.get(sid) == _SIGNAL_STATUS_CODE_CLOSED:
continue
linked_signal_states[sid] = state
linked_signal_charter_name[sid] = c.title
normalized_status_filter = _normalize_signal_status_filter(status)
created_from_utc, created_to_utc = _build_signal_created_at_range(
created_from=created_from,
created_to=created_to,
)
rows = []
for s in signals:
strategic_payload = _as_dict(s.strategic_analysis)
ai_analysis = _signal_ai_analysis(s)
source_context = _signal_source_context(s)
project_scope = _signal_project_scope(s)
proposer_name = _signal_proposer_name(s)
title_text = _signal_title_text(s)
normalized_status = str(s.status or "").strip()
sid = int(s.id)
status_code = _normalize_signal_status_code(
linked_state=linked_signal_states.get(sid),
signal_status=normalized_status,
processed=bool(s.processed),
)
row_status = _signal_status_label_from_code(status_code, normalized_status)
locked_by_charter = sid in linked_signal_states or ("关联" in normalized_status)
can_create_charter = not locked_by_charter
charter_lock_reason = None if can_create_charter else _SIGNAL_LOCK_REASON
row = {
"id": str(s.id),
"title": title_text[:80] or "未命名信号",
"content": s.content,
"priority": s.priority,
"signal_type": _signal_display_type(s),
"source": s.source,
"project_creator_name": project_creator_name,
"proposer_name": proposer_name,
"proposer_open_id": source_context.get("sender_open_id"),
"project_scope": project_scope,
"scope_reason": ai_analysis.get("reason"),
"target_project_hints": ai_analysis.get("target_project_hints", []),
"status": row_status,
"status_code": status_code,
"can_create_charter": can_create_charter,
"charter_lock_reason": charter_lock_reason,
"charter_name": linked_signal_charter_name.get(sid),
"created_at": s.created_at.isoformat() if s.created_at else None,
"attachments": _extract_signal_attachments(s),
"strategic_analysis": strategic_payload,
"planner_status": _normalize_planner_status(getattr(s, "planner_status", None)),
"planner_analyzed_at": (
s.planner_analyzed_at.isoformat() if getattr(s, "planner_analyzed_at", None) else None
),
"planner_assignee_user_id": (
str(s.planner_assignee_user_id) if getattr(s, "planner_assignee_user_id", None) is not None else None
),
}
created_at_dt = s.created_at
if created_at_dt is not None and created_at_dt.tzinfo is not None:
created_at_dt = created_at_dt.astimezone(CN_TZ).replace(tzinfo=None)
if not _match_signal_filters(
row=row,
status_filter=normalized_status_filter,
keyword=keyword,
source=source,
priority=priority,
created_from_utc=created_from_utc,
created_to_utc=created_to_utc,
created_at_dt=created_at_dt,
):
continue
rows.append(row)
total = len(rows)
paged_rows = rows[offset : offset + limit]
return success_response(
{
"items": paged_rows,
"total": total,
"limit": limit,
"offset": offset,
"has_more": (offset + len(paged_rows)) < total,
}
)