Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd21a82168 | |||
| 3823c9a2e4 |
@@ -3,11 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
from celery.schedules import crontab
|
||||||
from kombu import Queue
|
from kombu import Queue
|
||||||
|
|
||||||
from fastapi_admin.config import (
|
from fastapi_admin.config import (
|
||||||
LEAUDIT_TASK_SOFT_TIME_LIMIT,
|
LEAUDIT_TASK_SOFT_TIME_LIMIT,
|
||||||
LEAUDIT_TASK_TIME_LIMIT,
|
LEAUDIT_TASK_TIME_LIMIT,
|
||||||
|
LEAUDIT_STUCK_SCAN_CRON_MINUTES,
|
||||||
LEAUDIT_WORKER_QUEUE_NORMAL,
|
LEAUDIT_WORKER_QUEUE_NORMAL,
|
||||||
LEAUDIT_WORKER_QUEUE_URGENT,
|
LEAUDIT_WORKER_QUEUE_URGENT,
|
||||||
REDIS_DB,
|
REDIS_DB,
|
||||||
@@ -41,10 +43,18 @@ celery_app.conf.update(
|
|||||||
broker_connection_retry_on_startup=True,
|
broker_connection_retry_on_startup=True,
|
||||||
task_soft_time_limit=LEAUDIT_TASK_SOFT_TIME_LIMIT,
|
task_soft_time_limit=LEAUDIT_TASK_SOFT_TIME_LIMIT,
|
||||||
task_time_limit=LEAUDIT_TASK_TIME_LIMIT,
|
task_time_limit=LEAUDIT_TASK_TIME_LIMIT,
|
||||||
|
beat_schedule={
|
||||||
|
"leaudit-scan-stuck-documents": {
|
||||||
|
"task": "leaudit.scan_stuck_documents",
|
||||||
|
"schedule": crontab(minute=f"*/{max(1, int(LEAUDIT_STUCK_SCAN_CRON_MINUTES))}"),
|
||||||
|
"options": {"queue": LEAUDIT_WORKER_QUEUE_NORMAL},
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
celery_app.autodiscover_tasks(
|
celery_app.autodiscover_tasks(
|
||||||
[
|
[
|
||||||
"fastapi_modules.fastapi_leaudit.leaudit_bridge",
|
"fastapi_modules.fastapi_leaudit.leaudit_bridge",
|
||||||
]
|
],
|
||||||
|
force=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ LEAUDIT_WORKER_QUEUE_URGENT: str
|
|||||||
LEAUDIT_WORKER_QUEUE_NORMAL: str
|
LEAUDIT_WORKER_QUEUE_NORMAL: str
|
||||||
LEAUDIT_WORKER_CONCURRENCY: int
|
LEAUDIT_WORKER_CONCURRENCY: int
|
||||||
LEAUDIT_RUN_LOCK_SECONDS: int
|
LEAUDIT_RUN_LOCK_SECONDS: int
|
||||||
|
LEAUDIT_STUCK_SCAN_CRON_MINUTES: int
|
||||||
|
LEAUDIT_STUCK_TIMEOUT_MINUTES: int
|
||||||
LEAUDIT_TASK_SOFT_TIME_LIMIT: int
|
LEAUDIT_TASK_SOFT_TIME_LIMIT: int
|
||||||
LEAUDIT_TASK_TIME_LIMIT: int
|
LEAUDIT_TASK_TIME_LIMIT: int
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ class LeauditSettings(_Base):
|
|||||||
LEAUDIT_WORKER_QUEUE_NORMAL: str = "leaudit.normal"
|
LEAUDIT_WORKER_QUEUE_NORMAL: str = "leaudit.normal"
|
||||||
LEAUDIT_WORKER_CONCURRENCY: int = 2
|
LEAUDIT_WORKER_CONCURRENCY: int = 2
|
||||||
LEAUDIT_RUN_LOCK_SECONDS: int = 1800
|
LEAUDIT_RUN_LOCK_SECONDS: int = 1800
|
||||||
|
LEAUDIT_STUCK_SCAN_CRON_MINUTES: int = 5
|
||||||
|
LEAUDIT_STUCK_TIMEOUT_MINUTES: int = 20
|
||||||
LEAUDIT_TASK_SOFT_TIME_LIMIT: int = 3300
|
LEAUDIT_TASK_SOFT_TIME_LIMIT: int = 3300
|
||||||
LEAUDIT_TASK_TIME_LIMIT: int = 3600
|
LEAUDIT_TASK_TIME_LIMIT: int = 3600
|
||||||
|
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ class CrossReviewTaskDocumentVO(BaseModel):
|
|||||||
name: str = Field("", description="文档名称")
|
name: str = Field("", description="文档名称")
|
||||||
documentNumber: str | None = Field(None, description="文号")
|
documentNumber: str | None = Field(None, description="文号")
|
||||||
typeId: int | None = Field(None, description="文档类型ID")
|
typeId: int | None = Field(None, description="文档类型ID")
|
||||||
|
typeName: str | None = Field(None, description="文档类型名称")
|
||||||
processingStatus: str | None = Field(None, description="处理状态")
|
processingStatus: str | None = Field(None, description="处理状态")
|
||||||
versionNo: int = Field(1, description="版本号")
|
versionNo: int = Field(1, description="版本号")
|
||||||
isLatestVersion: bool = Field(True, description="是否最新版本")
|
isLatestVersion: bool = Field(True, description="是否最新版本")
|
||||||
auditStatus: int = Field(0, description="任务内完成状态")
|
auditStatus: int = Field(0, description="任务内完成状态")
|
||||||
createdAt: datetime | None = Field(None, description="创建时间")
|
createdAt: datetime | None = Field(None, description="创建时间")
|
||||||
|
fileSize: int = Field(0, description="文件大小(字节)")
|
||||||
|
|
||||||
|
|
||||||
class CrossReviewTaskDocumentPageVO(BaseModel):
|
class CrossReviewTaskDocumentPageVO(BaseModel):
|
||||||
|
|||||||
@@ -359,7 +359,9 @@ class StorageAdapter:
|
|||||||
"level": level,
|
"level": level,
|
||||||
"error_code": error_code,
|
"error_code": error_code,
|
||||||
"message": message,
|
"message": message,
|
||||||
"detail_json": detail_json,
|
"detail_json": json.dumps(detail_json, ensure_ascii=False)
|
||||||
|
if detail_json is not None
|
||||||
|
else None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from sqlalchemy import select
|
|||||||
from fastapi_admin.celery_app import celery_app
|
from fastapi_admin.celery_app import celery_app
|
||||||
from fastapi_admin.config import (
|
from fastapi_admin.config import (
|
||||||
LEAUDIT_RULES_DIR,
|
LEAUDIT_RULES_DIR,
|
||||||
|
LEAUDIT_STUCK_TIMEOUT_MINUTES,
|
||||||
LEAUDIT_WORKER_QUEUE_NORMAL,
|
LEAUDIT_WORKER_QUEUE_NORMAL,
|
||||||
LEAUDIT_WORKER_QUEUE_URGENT,
|
LEAUDIT_WORKER_QUEUE_URGENT,
|
||||||
)
|
)
|
||||||
@@ -256,6 +257,22 @@ def leaudit_process_document_task(self, run_id: int, rules_path: str | None = No
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(
|
||||||
|
bind=True,
|
||||||
|
name="leaudit.scan_stuck_documents",
|
||||||
|
)
|
||||||
|
def leaudit_scan_stuck_documents_task(self) -> dict[str, Any]:
|
||||||
|
"""周期扫描长时间无进展的评查文档,并自动标记失败。"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return loop.run_until_complete(
|
||||||
|
_scan_and_fail_stuck_documents(timeout_minutes=max(1, int(LEAUDIT_STUCK_TIMEOUT_MINUTES)))
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
# type_id → rules directory mapping (only fixed-mapping types)
|
# type_id → rules directory mapping (only fixed-mapping types)
|
||||||
# 行政许可 (type_id=2) has 9 sub-types, NOT mapped here —
|
# 行政许可 (type_id=2) has 9 sub-types, NOT mapped here —
|
||||||
# must come from document metadata (rules_file_path) or content classification.
|
# must come from document metadata (rules_file_path) or content classification.
|
||||||
@@ -633,3 +650,146 @@ def _queue_label(queue_name: str | None) -> str:
|
|||||||
if queue_name == LEAUDIT_WORKER_QUEUE_URGENT:
|
if queue_name == LEAUDIT_WORKER_QUEUE_URGENT:
|
||||||
return "urgent"
|
return "urgent"
|
||||||
return "normal"
|
return "normal"
|
||||||
|
|
||||||
|
|
||||||
|
async def _scan_and_fail_stuck_documents(*, timeout_minutes: int) -> dict[str, Any]:
|
||||||
|
"""扫描当前 run 长时间无更新时间的文档,并将其标记为失败。"""
|
||||||
|
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||||
|
from sqlalchemy import text as sa_text
|
||||||
|
|
||||||
|
lock_key = 20260512
|
||||||
|
timed_out_rows: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
lock_row = (
|
||||||
|
await session.execute(
|
||||||
|
sa_text("SELECT pg_try_advisory_lock(:lock_key)"),
|
||||||
|
{"lock_key": lock_key},
|
||||||
|
)
|
||||||
|
).fetchone()
|
||||||
|
has_lock = bool(lock_row[0]) if lock_row else False
|
||||||
|
if not has_lock:
|
||||||
|
log.info("stuck-scan skipped: advisory lock already held")
|
||||||
|
return {
|
||||||
|
"status": "skipped",
|
||||||
|
"reason": "lock_not_acquired",
|
||||||
|
"timeout_minutes": timeout_minutes,
|
||||||
|
"matched": 0,
|
||||||
|
"failed": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
rows = (
|
||||||
|
await session.execute(
|
||||||
|
sa_text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
d.id AS document_id,
|
||||||
|
d.processing_status,
|
||||||
|
d.current_run_id,
|
||||||
|
d.updated_at AS document_updated_at,
|
||||||
|
d.region,
|
||||||
|
d.normalized_name,
|
||||||
|
ar.id AS run_id,
|
||||||
|
ar.status AS run_status,
|
||||||
|
ar.phase,
|
||||||
|
ar.task_id,
|
||||||
|
ar.updated_at AS run_updated_at,
|
||||||
|
EXTRACT(EPOCH FROM (
|
||||||
|
NOW() - CASE
|
||||||
|
WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
|
||||||
|
THEN COALESCE(ar.updated_at, d.updated_at)
|
||||||
|
ELSE COALESCE(d.updated_at, ar.updated_at)
|
||||||
|
END
|
||||||
|
))::bigint AS idle_seconds
|
||||||
|
FROM leaudit_documents d
|
||||||
|
LEFT JOIN leaudit_audit_runs ar
|
||||||
|
ON ar.id = d.current_run_id
|
||||||
|
WHERE d.deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
|
||||||
|
OR LOWER(COALESCE(d.processing_status, '')) IN ('waiting', 'queued', 'running', 'processing')
|
||||||
|
)
|
||||||
|
AND CASE
|
||||||
|
WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
|
||||||
|
THEN COALESCE(ar.updated_at, d.updated_at)
|
||||||
|
ELSE COALESCE(d.updated_at, ar.updated_at)
|
||||||
|
END < NOW() - make_interval(mins => :timeout_minutes)
|
||||||
|
ORDER BY CASE
|
||||||
|
WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
|
||||||
|
THEN COALESCE(ar.updated_at, d.updated_at)
|
||||||
|
ELSE COALESCE(d.updated_at, ar.updated_at)
|
||||||
|
END ASC,
|
||||||
|
d.id ASC
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"timeout_minutes": timeout_minutes},
|
||||||
|
)
|
||||||
|
).mappings().all()
|
||||||
|
|
||||||
|
timed_out_rows = [dict(row) for row in rows]
|
||||||
|
finally:
|
||||||
|
await session.execute(
|
||||||
|
sa_text("SELECT pg_advisory_unlock(:lock_key)"),
|
||||||
|
{"lock_key": lock_key},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
storage = StorageAdapter()
|
||||||
|
failed_items: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for row in timed_out_rows:
|
||||||
|
document_id = int(row["document_id"])
|
||||||
|
run_id = int(row["run_id"]) if row.get("run_id") is not None else None
|
||||||
|
idle_seconds = int(row.get("idle_seconds") or 0)
|
||||||
|
run_phase = row.get("phase")
|
||||||
|
run_status = row.get("run_status")
|
||||||
|
processing_status = row.get("processing_status")
|
||||||
|
file_name = row.get("normalized_name") or f"document-{document_id}"
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"文档处理长时间无进展,已自动终止:"
|
||||||
|
f"document_id={document_id}, run_id={run_id}, "
|
||||||
|
f"processing_status={processing_status}, run_status={run_status}, "
|
||||||
|
f"phase={run_phase}, idle_seconds={idle_seconds}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _update_status_safe(document_id, "failed")
|
||||||
|
if run_id is not None:
|
||||||
|
await storage.fail_run(
|
||||||
|
document_id,
|
||||||
|
run_id=run_id,
|
||||||
|
phase=run_phase or "dispatch",
|
||||||
|
message=message,
|
||||||
|
detail_json={
|
||||||
|
"reason": "stuck_timeout",
|
||||||
|
"timeoutMinutes": timeout_minutes,
|
||||||
|
"idleSeconds": idle_seconds,
|
||||||
|
"taskId": row.get("task_id"),
|
||||||
|
"runStatus": run_status,
|
||||||
|
"processingStatus": processing_status,
|
||||||
|
"phase": run_phase,
|
||||||
|
"fileName": file_name,
|
||||||
|
"region": row.get("region"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
failed_items.append(
|
||||||
|
{
|
||||||
|
"document_id": document_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
"phase": run_phase,
|
||||||
|
"idle_seconds": idle_seconds,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
log.warning("stuck document auto-failed: %s", message)
|
||||||
|
except Exception:
|
||||||
|
log.exception("stuck document auto-fail failed: document_id=%s run_id=%s", document_id, run_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"timeout_minutes": timeout_minutes,
|
||||||
|
"matched": len(timed_out_rows),
|
||||||
|
"failed": len(failed_items),
|
||||||
|
"items": failed_items,
|
||||||
|
}
|
||||||
|
|||||||
@@ -414,14 +414,35 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
COALESCE(d.normalized_name, '') AS name,
|
COALESCE(d.normalized_name, '') AS name,
|
||||||
CAST(d.biz_document_id AS TEXT) AS document_number,
|
CAST(d.biz_document_id AS TEXT) AS document_number,
|
||||||
d.type_id,
|
d.type_id,
|
||||||
|
COALESCE(
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(COALESCE(ar.status, '')) IN ('pending', 'queued', 'running', 'retrying')
|
||||||
|
THEN ar.status
|
||||||
|
ELSE d.processing_status
|
||||||
|
END,
|
||||||
d.processing_status,
|
d.processing_status,
|
||||||
|
'waiting'
|
||||||
|
) AS processing_status,
|
||||||
d.version_no,
|
d.version_no,
|
||||||
d.is_latest_version,
|
d.is_latest_version,
|
||||||
d.created_at,
|
d.created_at,
|
||||||
td.audit_status
|
td.audit_status,
|
||||||
|
COALESCE(dt.name, '') AS type_name,
|
||||||
|
COALESCE(df.file_size, 0) AS file_size
|
||||||
FROM leaudit_cross_review_task_documents td
|
FROM leaudit_cross_review_task_documents td
|
||||||
JOIN leaudit_documents d
|
JOIN leaudit_documents d
|
||||||
ON d.id = td.document_id
|
ON d.id = td.document_id
|
||||||
|
LEFT JOIN leaudit_audit_runs ar
|
||||||
|
ON ar.id = d.current_run_id
|
||||||
|
LEFT JOIN leaudit_document_types dt
|
||||||
|
ON dt.id = d.type_id
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT file_size
|
||||||
|
FROM leaudit_document_files
|
||||||
|
WHERE document_id = d.id
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
) df ON TRUE
|
||||||
WHERE {whereSql}
|
WHERE {whereSql}
|
||||||
ORDER BY d.created_at DESC, d.id DESC
|
ORDER BY d.created_at DESC, d.id DESC
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
@@ -437,11 +458,13 @@ class CrossReviewServiceImpl(ICrossReviewService):
|
|||||||
name=str(row["name"] or ""),
|
name=str(row["name"] or ""),
|
||||||
documentNumber=row.get("document_number"),
|
documentNumber=row.get("document_number"),
|
||||||
typeId=self._to_int(row.get("type_id")),
|
typeId=self._to_int(row.get("type_id")),
|
||||||
|
typeName=row.get("type_name"),
|
||||||
processingStatus=row.get("processing_status"),
|
processingStatus=row.get("processing_status"),
|
||||||
versionNo=int(row.get("version_no") or 1),
|
versionNo=int(row.get("version_no") or 1),
|
||||||
isLatestVersion=bool(row.get("is_latest_version")),
|
isLatestVersion=bool(row.get("is_latest_version")),
|
||||||
auditStatus=int(row.get("audit_status") or 0),
|
auditStatus=int(row.get("audit_status") or 0),
|
||||||
createdAt=row.get("created_at"),
|
createdAt=row.get("created_at"),
|
||||||
|
fileSize=int(row.get("file_size") or 0),
|
||||||
)
|
)
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|||||||
+138
-16
@@ -4,13 +4,16 @@ PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
FRONTEND_DIR="$PROJECT_DIR/legal-platform-frontend"
|
FRONTEND_DIR="$PROJECT_DIR/legal-platform-frontend"
|
||||||
BACKEND_DIR="$PROJECT_DIR"
|
BACKEND_DIR="$PROJECT_DIR"
|
||||||
WORKER_SCRIPT="$PROJECT_DIR/scripts/start_worker.sh"
|
WORKER_SCRIPT="$PROJECT_DIR/scripts/start_worker.sh"
|
||||||
|
BEAT_SCRIPT="$PROJECT_DIR/scripts/start_beat.sh"
|
||||||
LOG_DIR="$PROJECT_DIR/.codex-run"
|
LOG_DIR="$PROJECT_DIR/.codex-run"
|
||||||
BACKEND_PID_FILE="$LOG_DIR/backend.pid"
|
BACKEND_PID_FILE="$LOG_DIR/backend.pid"
|
||||||
FRONTEND_PID_FILE="$LOG_DIR/frontend.pid"
|
FRONTEND_PID_FILE="$LOG_DIR/frontend.pid"
|
||||||
WORKER_PID_FILE="$LOG_DIR/worker.pid"
|
WORKER_PID_FILE="$LOG_DIR/worker.pid"
|
||||||
|
BEAT_PID_FILE="$LOG_DIR/beat.pid"
|
||||||
BACKEND_LOG="$LOG_DIR/backend.log"
|
BACKEND_LOG="$LOG_DIR/backend.log"
|
||||||
FRONTEND_LOG="$LOG_DIR/frontend.log"
|
FRONTEND_LOG="$LOG_DIR/frontend.log"
|
||||||
WORKER_LOG="$LOG_DIR/worker.log"
|
WORKER_LOG="$LOG_DIR/worker.log"
|
||||||
|
BEAT_LOG="$LOG_DIR/beat.log"
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
@@ -113,6 +116,22 @@ pid_command() {
|
|||||||
ps -p "$pid" -o args= 2>/dev/null | xargs
|
ps -p "$pid" -o args= 2>/dev/null | xargs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kill_process_tree() {
|
||||||
|
local pid=$1
|
||||||
|
[ -n "$pid" ] || return 0
|
||||||
|
|
||||||
|
local children
|
||||||
|
children=$(pgrep -P "$pid" 2>/dev/null || true)
|
||||||
|
if [ -n "$children" ]; then
|
||||||
|
local child
|
||||||
|
for child in $children; do
|
||||||
|
kill_process_tree "$child"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
ensure_port_free() {
|
ensure_port_free() {
|
||||||
local name=$1
|
local name=$1
|
||||||
local port=$2
|
local port=$2
|
||||||
@@ -153,10 +172,12 @@ start_backend() {
|
|||||||
exec "$BACKEND_PYTHON" run.py
|
exec "$BACKEND_PYTHON" run.py
|
||||||
) >> "$BACKEND_LOG" 2>&1 &
|
) >> "$BACKEND_LOG" 2>&1 &
|
||||||
pid=$!
|
pid=$!
|
||||||
echo "$pid" > "$BACKEND_PID_FILE"
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
if pid_alive "$pid"; then
|
local listening_pid
|
||||||
|
listening_pid=$(port_pid "$BACKEND_PORT")
|
||||||
|
if pid_alive "$pid" || [ -n "$listening_pid" ]; then
|
||||||
|
echo "${listening_pid:-$pid}" > "$BACKEND_PID_FILE"
|
||||||
log_success "后端启动成功 (PID: $pid, 端口: $BACKEND_PORT)"
|
log_success "后端启动成功 (PID: $pid, 端口: $BACKEND_PORT)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -185,10 +206,12 @@ start_frontend() {
|
|||||||
exec npm run dev:dev
|
exec npm run dev:dev
|
||||||
) >> "$FRONTEND_LOG" 2>&1 &
|
) >> "$FRONTEND_LOG" 2>&1 &
|
||||||
pid=$!
|
pid=$!
|
||||||
echo "$pid" > "$FRONTEND_PID_FILE"
|
|
||||||
sleep 4
|
sleep 4
|
||||||
|
|
||||||
if pid_alive "$pid"; then
|
local listening_pid
|
||||||
|
listening_pid=$(port_pid "$FRONTEND_DEV_PORT")
|
||||||
|
if pid_alive "$pid" || [ -n "$listening_pid" ]; then
|
||||||
|
echo "${listening_pid:-$pid}" > "$FRONTEND_PID_FILE"
|
||||||
log_success "前端启动成功 (PID: $pid, 开发端口: $FRONTEND_DEV_PORT, 访问端口: $FRONTEND_PUBLIC_PORT)"
|
log_success "前端启动成功 (PID: $pid, 开发端口: $FRONTEND_DEV_PORT, 访问端口: $FRONTEND_PUBLIC_PORT)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -234,29 +257,89 @@ start_worker() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start_beat() {
|
||||||
|
cleanup_pid_file "$BEAT_PID_FILE"
|
||||||
|
local pid
|
||||||
|
pid=$(service_pid "$BEAT_PID_FILE")
|
||||||
|
if pid_alive "$pid"; then
|
||||||
|
log_warn "Beat 已在运行 (PID: $pid)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$BEAT_SCRIPT" ]; then
|
||||||
|
log_error "Beat 启动脚本不存在或不可执行: $BEAT_SCRIPT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "启动 Beat 调度服务..."
|
||||||
|
: > "$BEAT_LOG"
|
||||||
|
(
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
exec "$BEAT_SCRIPT"
|
||||||
|
) >> "$BEAT_LOG" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
echo "$pid" > "$BEAT_PID_FILE"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if pid_alive "$pid"; then
|
||||||
|
log_success "Beat 启动成功 (PID: $pid)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_error "Beat 启动失败,查看日志: $BEAT_LOG"
|
||||||
|
tail -20 "$BEAT_LOG" 2>/dev/null || true
|
||||||
|
rm -f "$BEAT_PID_FILE"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
stop_service() {
|
stop_service() {
|
||||||
local name=$1
|
local name=$1
|
||||||
local pid_file=$2
|
local pid_file=$2
|
||||||
|
local port=${3:-}
|
||||||
local pid
|
local pid
|
||||||
pid=$(service_pid "$pid_file")
|
pid=$(service_pid "$pid_file")
|
||||||
|
|
||||||
if ! pid_alive "$pid"; then
|
if ! pid_alive "$pid"; then
|
||||||
|
if [ -n "$port" ]; then
|
||||||
|
local stray_pid
|
||||||
|
stray_pid=$(port_pid "$port")
|
||||||
|
if [ -n "$stray_pid" ]; then
|
||||||
|
log_warn "$name PID 文件已失效,正在清理端口 $port 上的残留进程 (PID: $stray_pid)..."
|
||||||
|
kill_process_tree "$stray_pid"
|
||||||
|
sleep 1
|
||||||
|
if pid_alive "$stray_pid"; then
|
||||||
|
kill -9 "$stray_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f "$pid_file"
|
||||||
|
log_success "$name 残留进程已停止"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
rm -f "$pid_file"
|
rm -f "$pid_file"
|
||||||
log_warn "$name 未运行"
|
log_warn "$name 未运行"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "停止 $name (PID: $pid)..."
|
log_info "停止 $name (PID: $pid)..."
|
||||||
kill "$pid" 2>/dev/null || true
|
kill_process_tree "$pid"
|
||||||
for _ in $(seq 1 10); do
|
for _ in $(seq 1 10); do
|
||||||
if ! pid_alive "$pid"; then
|
local current_port_pid=""
|
||||||
|
if [ -n "$port" ]; then
|
||||||
|
current_port_pid=$(port_pid "$port")
|
||||||
|
fi
|
||||||
|
if ! pid_alive "$pid" && [ -z "$current_port_pid" ]; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
done
|
done
|
||||||
if pid_alive "$pid"; then
|
local stubborn_pid=""
|
||||||
|
if [ -n "$port" ]; then
|
||||||
|
stubborn_pid=$(port_pid "$port")
|
||||||
|
fi
|
||||||
|
if pid_alive "$pid" || [ -n "$stubborn_pid" ]; then
|
||||||
log_warn "$name 未响应,强制终止..."
|
log_warn "$name 未响应,强制终止..."
|
||||||
kill -9 "$pid" 2>/dev/null || true
|
kill -9 "$pid" 2>/dev/null || true
|
||||||
|
[ -n "$stubborn_pid" ] && kill -9 "$stubborn_pid" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
rm -f "$pid_file"
|
rm -f "$pid_file"
|
||||||
log_success "$name 已停止"
|
log_success "$name 已停止"
|
||||||
@@ -272,12 +355,14 @@ do_start() {
|
|||||||
start_backend || return 1
|
start_backend || return 1
|
||||||
start_frontend || return 1
|
start_frontend || return 1
|
||||||
start_worker || return 1
|
start_worker || return 1
|
||||||
|
start_beat || return 1
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}============================================${NC}"
|
echo -e "${GREEN}============================================${NC}"
|
||||||
echo -e "${GREEN} 前端: http://localhost:$FRONTEND_PUBLIC_PORT (开发服务: $FRONTEND_DEV_PORT)${NC}"
|
echo -e "${GREEN} 前端: http://localhost:$FRONTEND_PUBLIC_PORT (开发服务: $FRONTEND_DEV_PORT)${NC}"
|
||||||
echo -e "${GREEN} 后端: http://localhost:$BACKEND_PORT${NC}"
|
echo -e "${GREEN} 后端: http://localhost:$BACKEND_PORT${NC}"
|
||||||
echo -e "${GREEN} Worker: $WORKER_SCRIPT${NC}"
|
echo -e "${GREEN} Worker: $WORKER_SCRIPT${NC}"
|
||||||
|
echo -e "${GREEN} Beat: $BEAT_SCRIPT${NC}"
|
||||||
echo -e "${GREEN} 日志目录: $LOG_DIR${NC}"
|
echo -e "${GREEN} 日志目录: $LOG_DIR${NC}"
|
||||||
echo -e "${GREEN}============================================${NC}"
|
echo -e "${GREEN}============================================${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -289,9 +374,10 @@ do_stop() {
|
|||||||
echo -e "${YELLOW} 停止 LeAudit 前后端${NC}"
|
echo -e "${YELLOW} 停止 LeAudit 前后端${NC}"
|
||||||
echo -e "${YELLOW}============================================${NC}"
|
echo -e "${YELLOW}============================================${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
stop_service "Beat" "$BEAT_PID_FILE"
|
||||||
stop_service "Worker" "$WORKER_PID_FILE"
|
stop_service "Worker" "$WORKER_PID_FILE"
|
||||||
stop_service "前端" "$FRONTEND_PID_FILE"
|
stop_service "前端" "$FRONTEND_PID_FILE" "$FRONTEND_DEV_PORT"
|
||||||
stop_service "后端" "$BACKEND_PID_FILE"
|
stop_service "后端" "$BACKEND_PID_FILE" "$BACKEND_PORT"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,6 +391,7 @@ do_status() {
|
|||||||
cleanup_pid_file "$BACKEND_PID_FILE"
|
cleanup_pid_file "$BACKEND_PID_FILE"
|
||||||
cleanup_pid_file "$FRONTEND_PID_FILE"
|
cleanup_pid_file "$FRONTEND_PID_FILE"
|
||||||
cleanup_pid_file "$WORKER_PID_FILE"
|
cleanup_pid_file "$WORKER_PID_FILE"
|
||||||
|
cleanup_pid_file "$BEAT_PID_FILE"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}============================================${NC}"
|
echo -e "${CYAN}============================================${NC}"
|
||||||
@@ -334,10 +421,18 @@ do_status() {
|
|||||||
echo -e " Worker: ${RED}○ 已停止${NC}"
|
echo -e " Worker: ${RED}○ 已停止${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
pid=$(service_pid "$BEAT_PID_FILE")
|
||||||
|
if pid_alive "$pid"; then
|
||||||
|
echo -e " Beat: ${GREEN}● 运行中${NC} PID=$pid"
|
||||||
|
else
|
||||||
|
echo -e " Beat: ${RED}○ 已停止${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " 后端日志: $BACKEND_LOG"
|
echo " 后端日志: $BACKEND_LOG"
|
||||||
echo " 前端日志: $FRONTEND_LOG"
|
echo " 前端日志: $FRONTEND_LOG"
|
||||||
echo " Worker日志: $WORKER_LOG"
|
echo " Worker日志: $WORKER_LOG"
|
||||||
|
echo " Beat日志: $BEAT_LOG"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,10 +462,18 @@ do_logs() {
|
|||||||
echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}"
|
echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}"
|
||||||
tail -f "$WORKER_LOG"
|
tail -f "$WORKER_LOG"
|
||||||
;;
|
;;
|
||||||
|
beat)
|
||||||
|
[ -f "$BEAT_LOG" ] || touch "$BEAT_LOG"
|
||||||
|
echo -e "${CYAN}--- Beat 日志 (最近 $lines 行) ---${NC}"
|
||||||
|
tail -n "$lines" "$BEAT_LOG"
|
||||||
|
echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}"
|
||||||
|
tail -f "$BEAT_LOG"
|
||||||
|
;;
|
||||||
all)
|
all)
|
||||||
[ -f "$BACKEND_LOG" ] || touch "$BACKEND_LOG"
|
[ -f "$BACKEND_LOG" ] || touch "$BACKEND_LOG"
|
||||||
[ -f "$FRONTEND_LOG" ] || touch "$FRONTEND_LOG"
|
[ -f "$FRONTEND_LOG" ] || touch "$FRONTEND_LOG"
|
||||||
[ -f "$WORKER_LOG" ] || touch "$WORKER_LOG"
|
[ -f "$WORKER_LOG" ] || touch "$WORKER_LOG"
|
||||||
|
[ -f "$BEAT_LOG" ] || touch "$BEAT_LOG"
|
||||||
echo -e "${CYAN}--- 后端日志 (最近 $lines 行) ---${NC}"
|
echo -e "${CYAN}--- 后端日志 (最近 $lines 行) ---${NC}"
|
||||||
tail -n "$lines" "$BACKEND_LOG"
|
tail -n "$lines" "$BACKEND_LOG"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -380,16 +483,20 @@ do_logs() {
|
|||||||
echo -e "${CYAN}--- Worker 日志 (最近 $lines 行) ---${NC}"
|
echo -e "${CYAN}--- Worker 日志 (最近 $lines 行) ---${NC}"
|
||||||
tail -n "$lines" "$WORKER_LOG"
|
tail -n "$lines" "$WORKER_LOG"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e "${CYAN}--- Beat 日志 (最近 $lines 行) ---${NC}"
|
||||||
|
tail -n "$lines" "$BEAT_LOG"
|
||||||
|
echo ""
|
||||||
echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}"
|
echo -e "${CYAN}--- Ctrl+C 退出 ---${NC}"
|
||||||
tail -n 0 -f "$BACKEND_LOG" "$FRONTEND_LOG" "$WORKER_LOG" 2>/dev/null | awk '
|
tail -n 0 -f "$BACKEND_LOG" "$FRONTEND_LOG" "$WORKER_LOG" "$BEAT_LOG" 2>/dev/null | awk '
|
||||||
/^==> .*backend\.log <==$/ { current="[backend]"; next }
|
/^==> .*backend\.log <==$/ { current="[backend]"; next }
|
||||||
/^==> .*frontend\.log <==$/ { current="[frontend]"; next }
|
/^==> .*frontend\.log <==$/ { current="[frontend]"; next }
|
||||||
/^==> .*worker\.log <==$/ { current="[worker]"; next }
|
/^==> .*worker\.log <==$/ { current="[worker]"; next }
|
||||||
|
/^==> .*beat\.log <==$/ { current="[beat]"; next }
|
||||||
{ print current " " $0; fflush() }
|
{ print current " " $0; fflush() }
|
||||||
'
|
'
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "用法: ./leaudit.sh logs [backend|frontend|worker|all] [行数]"
|
echo "用法: ./leaudit.sh logs [backend|frontend|worker|beat|all] [行数]"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -441,6 +548,16 @@ do_doctor() {
|
|||||||
echo -e " Worker 进程: ${RED}○ 未运行${NC}"
|
echo -e " Worker 进程: ${RED}○ 未运行${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local beat_pid
|
||||||
|
beat_pid=$(service_pid "$BEAT_PID_FILE")
|
||||||
|
if pid_alive "$beat_pid"; then
|
||||||
|
cmd="$(pid_command "$beat_pid")"
|
||||||
|
echo -e " Beat 进程: ${GREEN}● 运行中${NC} PID=$beat_pid"
|
||||||
|
[ -n "$cmd" ] && echo " 命令: $cmd"
|
||||||
|
else
|
||||||
|
echo -e " Beat 进程: ${RED}○ 未运行${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +571,7 @@ do_open() {
|
|||||||
echo " 前端开发: http://127.0.0.1:$FRONTEND_DEV_PORT"
|
echo " 前端开发: http://127.0.0.1:$FRONTEND_DEV_PORT"
|
||||||
echo " 后端访问: http://localhost:$BACKEND_PORT"
|
echo " 后端访问: http://localhost:$BACKEND_PORT"
|
||||||
echo " Worker脚本: $WORKER_SCRIPT"
|
echo " Worker脚本: $WORKER_SCRIPT"
|
||||||
|
echo " Beat脚本: $BEAT_SCRIPT"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}--- 后端最近 5 行 ---${NC}"
|
echo -e "${CYAN}--- 后端最近 5 行 ---${NC}"
|
||||||
tail -n 5 "$BACKEND_LOG" 2>/dev/null || echo "(无后端日志)"
|
tail -n 5 "$BACKEND_LOG" 2>/dev/null || echo "(无后端日志)"
|
||||||
@@ -464,6 +582,9 @@ do_open() {
|
|||||||
echo -e "${CYAN}--- Worker最近 5 行 ---${NC}"
|
echo -e "${CYAN}--- Worker最近 5 行 ---${NC}"
|
||||||
tail -n 5 "$WORKER_LOG" 2>/dev/null || echo "(无Worker日志)"
|
tail -n 5 "$WORKER_LOG" 2>/dev/null || echo "(无Worker日志)"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e "${CYAN}--- Beat最近 5 行 ---${NC}"
|
||||||
|
tail -n 5 "$BEAT_LOG" 2>/dev/null || echo "(无Beat日志)"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
@@ -492,14 +613,15 @@ case "${1:-help}" in
|
|||||||
echo "用法: ./leaudit.sh <命令>"
|
echo "用法: ./leaudit.sh <命令>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "命令说明:"
|
echo "命令说明:"
|
||||||
echo " start 启动前后端"
|
echo " start 启动前后端、Worker、Beat"
|
||||||
echo " stop 停止前后端"
|
echo " stop 停止前后端、Worker、Beat"
|
||||||
echo " restart 重启前后端"
|
echo " restart 重启前后端、Worker、Beat"
|
||||||
echo " status 查看前后端运行状态"
|
echo " status 查看前后端、Worker、Beat 运行状态"
|
||||||
echo " logs 查看前后端日志并持续跟踪"
|
echo " logs 查看前后端、Worker、Beat 日志并持续跟踪"
|
||||||
echo " logs backend 只看后端日志"
|
echo " logs backend 只看后端日志"
|
||||||
echo " logs frontend 50 看前端最近 50 行日志并持续跟踪"
|
echo " logs frontend 50 看前端最近 50 行日志并持续跟踪"
|
||||||
echo " logs worker 50 看 Worker 最近 50 行日志并持续跟踪"
|
echo " logs worker 50 看 Worker 最近 50 行日志并持续跟踪"
|
||||||
|
echo " logs beat 50 看 Beat 最近 50 行日志并持续跟踪"
|
||||||
echo " doctor 检查 5173 / 5193 / 8096 端口占用情况"
|
echo " doctor 检查 5173 / 5193 / 8096 端口占用情况"
|
||||||
echo " open 打印访问地址和最近日志摘要"
|
echo " open 打印访问地址和最近日志摘要"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "leaudit-platform-dev-runner",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bash ./leaudit.sh start",
|
||||||
|
"dev:stop": "bash ./leaudit.sh stop",
|
||||||
|
"dev:restart": "bash ./leaudit.sh restart",
|
||||||
|
"dev:status": "bash ./leaudit.sh status",
|
||||||
|
"dev:logs": "bash ./leaudit.sh logs",
|
||||||
|
"dev:logs:frontend": "bash ./leaudit.sh logs frontend",
|
||||||
|
"dev:logs:backend": "bash ./leaudit.sh logs backend",
|
||||||
|
"dev:doctor": "bash ./leaudit.sh doctor",
|
||||||
|
"dev:open": "bash ./leaudit.sh open"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
SCHEDULE_DIR="$ROOT_DIR/.codex-run"
|
||||||
|
mkdir -p "$SCHEDULE_DIR"
|
||||||
|
|
||||||
|
celery -A fastapi_admin.celery_app:celery_app beat \
|
||||||
|
--loglevel=INFO \
|
||||||
|
--pidfile= \
|
||||||
|
--schedule "$SCHEDULE_DIR/celerybeat-schedule"
|
||||||
Reference in New Issue
Block a user