146 lines
6.5 KiB
Python
146 lines
6.5 KiB
Python
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,
|
||
}
|
||
)
|
||
|