Files
gloria/confluence/vessel_reports.py
Developer 5d0cafac32 feat: 交接班报告支持 Confluence/Jira 集成,添加 N/A 记录时间归属功能
- 集成 Confluence API 获取船舶报告数据
- 集成 Jira API 查询故障数量
- 支持船号显示 (462#、463# 等)
- 支持故障次数/故障率、人工介入次数/介入率显示
- 跨班作业使用 Card 69 按时间查询效率
- 不跨班作业使用整船效率(剔除异常)
- N/A 记录根据作业时间归属到对应船舶
- 更新 AGENTS.md 和 README.md 文档
- 删除 daily_report_gui.py
2026-03-14 02:52:23 +08:00

392 lines
13 KiB
Python
Raw Permalink 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.
"""
船舶报告管理器
从 Confluence 提取船舶报告数据,包括故障次数、人工介入次数等。
"""
import re
from typing import Optional, Dict, List, Any
from datetime import datetime, timedelta
from .client import ConfluenceClient
class VesselReportManager:
"""船舶报告数据管理器"""
# 月度统计页面父页面 ID
PARENT_PAGE_ID = "137446574" # "福州江阴实船作业统计"
# 日期范围映射 (根据实际页面结构调整)
MONTH_PAGE_MAPPING: Dict[str, str] = {}
def __init__(self, client: Optional[ConfluenceClient] = None):
"""
初始化船舶报告管理器
Args:
client: Confluence 客户端实例,为 None 时自动创建
"""
if client is None:
# 从环境变量获取配置
import os
base_url = os.getenv(
"CONFLUENCE_URL", "https://confluence.westwell-lab.com"
)
token = os.getenv("CONFLUENCE_TOKEN")
if not token:
raise ValueError("未设置 CONFLUENCE_TOKEN 环境变量")
self.client = ConfluenceClient(base_url, token)
else:
self.client = client
self.jira_client = None
def set_jira_client(self, jira_client):
self.jira_client = jira_client
def _extract_jira_jqls(self, body: str, vessel_number: str) -> List[str]:
jqls = []
jira_macros = re.findall(
r'<ac:structured-macro[^>]*ac:name="jira"[^>]*>(.*?)</ac:structured-macro>',
body,
re.DOTALL,
)
for macro in jira_macros:
jql_match = re.search(
r'<ac:parameter[^>]*name="jqlQuery"[^>]*>(.*?)</ac:parameter>',
macro,
re.DOTALL,
)
if jql_match:
jql = jql_match.group(1)
jql = (
jql.replace("&quot;", '"')
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&amp;", "&")
)
if f'实船船次 = "{vessel_number}"' in jql:
jqls.append(jql)
return jqls
def _count_issues_from_jira(self, body: str, vessel_number: Optional[str]) -> int:
if not self.jira_client or not vessel_number:
return 0
try:
jqls = self._extract_jira_jqls(body, vessel_number)
total_issues = 0
for jql in jqls:
count = self.jira_client.count_issues(jql)
total_issues += count
return total_issues
except Exception as e:
print(f"从 Jira 查询故障数量失败: {e}")
return 0
def _get_monthly_page_id(self, year_month: str) -> Optional[str]:
"""
获取指定年月的统计页面 ID
Args:
year_month: 年月格式 "YYYY.MM"
Returns:
页面 ID未找到返回 None
"""
# 首先检查缓存
if year_month in self.MONTH_PAGE_MAPPING:
return self.MONTH_PAGE_MAPPING[year_month]
# 获取父页面的子页面
children = self.client.get_child_pages(self.PARENT_PAGE_ID, limit=100)
for child in children:
title = child.get("title", "")
# 匹配标题格式: "2025.06 实船作业统计"
if f"{year_month} 实船作业统计" in title:
page_id = child.get("id")
if page_id:
self.MONTH_PAGE_MAPPING[year_month] = str(page_id)
return str(page_id)
return None
def _parse_vessel_page(self, page_data: Dict) -> Optional[Dict[str, Any]]:
"""
解析船舶报告页面数据
Args:
page_data: 页面数据字典
Returns:
解析后的船舶数据字典
"""
try:
title = page_data.get("title", "")
body = page_data.get("body", {}).get("storage", {}).get("value", "")
if not body:
return None
# 提取船次号
vessel_match = re.search(r"船次.*?<td[^>]*>(\d+)#", body, re.DOTALL)
vessel_number = vessel_match.group(1) if vessel_match else None
if not vessel_number:
# 尝试从标题提取
title_match = re.search(r"(\d+)#", title)
if title_match:
vessel_number = title_match.group(1)
# 提取船名
name_match = re.search(r"船名.*?<td[^>]*>([^<]+)", body, re.DOTALL)
vessel_name = name_match.group(1).strip() if name_match else ""
# 提取作业时间
time_match = re.search(r"作业时间.*?<td[^>]*>(.*?)</td>", body, re.DOTALL)
operation_time = ""
if time_match:
operation_time = re.sub(r"<[^>]+>", "", time_match.group(1)).strip()
operation_time = operation_time.replace("&nbsp;", " ")
# 提取 TEU
teu_match = re.search(r"作业箱量 \(TEU\).*?<td[^>]*>(\d+)", body, re.DOTALL)
teu = int(teu_match.group(1)) if teu_match else 0
# 提取故障次数
failure_match = re.search(r"故障次数.*?<td[^>]*>(\d+)", body, re.DOTALL)
failures = int(failure_match.group(1)) if failure_match else 0
if failures == 0 and vessel_number:
failures = self._count_issues_from_jira(body, vessel_number)
# 提取故障率(如有)
failure_rate_match = re.search(
r"故障率.*?<td[^>]*>([\d.]+)%", body, re.DOTALL
)
failure_rate = (
float(failure_rate_match.group(1)) if failure_rate_match else None
)
# 提取人工介入次数
intervention_match = re.search(
r"人工介入次数.*?<td[^>]*>(\d+)", body, re.DOTALL
)
interventions = (
int(intervention_match.group(1)) if intervention_match else 0
)
# 提取人工介入率(如有)
intervention_rate_match = re.search(
r"人工介入率.*?<td[^>]*>([\d.]+)%", body, re.DOTALL
)
intervention_rate = (
float(intervention_rate_match.group(1))
if intervention_rate_match
else None
)
# 提取上线车辆
vehicles_match = re.search(r"上线车辆.*?<td[^>]*>([^<]+)", body, re.DOTALL)
vehicles = vehicles_match.group(1).strip() if vehicles_match else ""
# 提取作业循环
moves_match = re.search(r"作业循环.*?<td[^>]*>(\d+)", body, re.DOTALL)
moves = int(moves_match.group(1)) if moves_match else 0
# 提取作业效率
efficiency_match = re.search(
r"作业净效率.*?<td[^>]*>([\d.]+)", body, re.DOTALL
)
efficiency = float(efficiency_match.group(1)) if efficiency_match else 0.0
# 提取作业类型
type_match = re.search(r"作业类型.*?<td[^>]*>([^<]+)", body, re.DOTALL)
operation_type = type_match.group(1).strip() if type_match else ""
# 解析作业日期(从标题或作业时间)
operation_date = None
# 尝试从标题提取日期: "FZ 433#实船报告2026.03.01"
date_match = re.search(r"(\d{4})\.(\d{2})\.(\d{2})", title)
if date_match:
try:
operation_date = f"{date_match.group(1)}-{date_match.group(2)}-{date_match.group(3)}"
except:
pass
# 计算故障率和人工介入率(如果页面上没有)
if failure_rate is None and teu > 0:
failure_rate = round((failures / (teu / 2)) * 100, 2)
if intervention_rate is None and teu > 0:
intervention_rate = round((interventions / (teu / 2)) * 100, 2)
return {
"vessel_number": vessel_number,
"vessel_name": vessel_name,
"vessel_code": vessel_number, # 兼容现有模板
"operation_date": operation_date,
"operation_time": operation_time,
"teu": teu,
"failures": failures,
"failure_rate": failure_rate if failure_rate is not None else 0.0,
"interventions": interventions,
"intervention_rate": intervention_rate
if intervention_rate is not None
else 0.0,
"vehicles": vehicles,
"vehicle_count": len(vehicles.split("")) if vehicles else 0,
"moves": moves,
"efficiency": efficiency,
"operation_type": operation_type,
"page_id": page_data.get("id"),
"page_title": title,
}
except Exception as e:
print(f"解析页面数据失败: {e}")
return None
def get_vessel_reports_by_month(self, year_month: str) -> List[Dict[str, Any]]:
"""
获取指定月份的所有船舶报告
Args:
year_month: 年月格式 "YYYY.MM"
Returns:
船舶报告列表
"""
month_page_id = self._get_monthly_page_id(year_month)
if not month_page_id:
print(f"未找到 {year_month} 的统计页面")
return []
# 获取该月份下的所有船舶报告页面
vessel_pages = self.client.get_child_pages(
month_page_id, limit=100, expand="body.storage"
)
reports = []
for page in vessel_pages:
report = self._parse_vessel_page(page)
if report:
reports.append(report)
# 按船次号排序
reports.sort(
key=lambda x: (
int(x.get("vessel_number", 0)) if x.get("vessel_number") else 0
)
)
return reports
def get_vessel_report_by_number(
self, vessel_number: str
) -> Optional[Dict[str, Any]]:
"""
根据船次号获取船舶报告
Args:
vessel_number: 船次号 (如 "433")
Returns:
船舶报告数据,未找到返回 None
"""
# 尝试获取最近几个月的数据
from datetime import datetime
now = datetime.now()
for i in range(3): # 最近3个月
year_month = (now - timedelta(days=30 * i)).strftime("%Y.%m")
reports = self.get_vessel_reports_by_month(year_month)
for report in reports:
if report.get("vessel_number") == vessel_number:
return report
return None
def get_vessel_reports_in_range(
self, start_date: str, end_date: str
) -> List[Dict[str, Any]]:
"""
获取指定日期范围内的所有船舶报告
Args:
start_date: 开始日期 "YYYY-MM-DD"
end_date: 结束日期 "YYYY-MM-DD"
Returns:
船舶报告列表
"""
try:
start = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
months_to_query = set()
current = start
while current <= end:
months_to_query.add(current.strftime("%Y.%m"))
current += timedelta(days=1)
all_reports = []
for year_month in sorted(months_to_query):
reports = self.get_vessel_reports_by_month(year_month)
all_reports.extend(reports)
filtered_reports = []
for report in all_reports:
report_date = report.get("operation_date")
if report_date:
try:
report_dt = datetime.strptime(report_date, "%Y-%m-%d")
if start <= report_dt <= end:
filtered_reports.append(report)
except:
pass
return filtered_reports
except ValueError as e:
print(f"日期解析错误: {e}")
return []
def get_vessel_reports_by_date(self, date_str: str) -> List[Dict[str, Any]]:
"""
获取指定日期的船舶报告
Args:
date_str: 日期格式 "YYYY-MM-DD"
Returns:
该日期的船舶报告列表
"""
return self.get_vessel_reports_in_range(date_str, date_str)
def get_vessel_page_id(self, vessel_number: str, year_month: str) -> Optional[str]:
"""
获取指定船次在指定月份的页面 ID
Args:
vessel_number: 船次号
year_month: 年月格式 "YYYY.MM"
Returns:
页面 ID未找到返回 None
"""
month_page_id = self._get_monthly_page_id(year_month)
if not month_page_id:
return None
vessel_pages = self.client.get_child_pages(month_page_id, limit=100)
for page in vessel_pages:
title = page.get("title", "")
# 匹配船次号: "FZ 433#实船报告..."
if f" {vessel_number}#" in title or f"{vessel_number}#" in title:
return page.get("id")
return None