feat: 交接班报告支持 Confluence/Jira 集成,添加 N/A 记录时间归属功能

- 集成 Confluence API 获取船舶报告数据
- 集成 Jira API 查询故障数量
- 支持船号显示 (462#、463# 等)
- 支持故障次数/故障率、人工介入次数/介入率显示
- 跨班作业使用 Card 69 按时间查询效率
- 不跨班作业使用整船效率(剔除异常)
- N/A 记录根据作业时间归属到对应船舶
- 更新 AGENTS.md 和 README.md 文档
- 删除 daily_report_gui.py
This commit is contained in:
Developer
2026-03-14 02:52:23 +08:00
parent cc989a8ddb
commit 5d0cafac32
13 changed files with 1746 additions and 394 deletions

View File

@@ -0,0 +1,391 @@
"""
船舶报告管理器
从 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