refactor(api): 使用常量替代硬编码字符串

- 在health_check接口中使用ApiResponseKey.STATUS和ApiStatus.OK常量
- 替换硬编码的状态返回值为枚举常量

refactor(core): 配置模块错误信息统一使用常量

- 从constants模块导入ConfigErrorDetail并替换CORS_ORIGINS和LEGACY_ALLOWED_QUERIES的验证错误信息
- 配置类中的默认值使用constants中定义的常量

feat(constants): 添加API响应、安全错误和配置错误常量类

- 新增ApiResponseKey用于API状态键名
- 新增ApiStatus用于API状态值
- 新增SecurityErrorDetail用于安全认证错误详情
- 新增ConfigErrorDetail用于配置验证错误详情
- 添加DEFAULT_MODEL_PROVIDER和DEFAULT_OPENCLAW_ACTION_JSON常量

refactor(security): 安全认证模块使用错误常量

- 将硬编码的安全错误信息替换为SecurityErrorDetail常量
- 包括API密钥、审批密钥和审计密钥的相关错误信息

refactor(ai-agent): AI代理适配器改进错误处理

- 将HTTP状态码替换为FastAPI状态常量
- 添加OpenClaw工具和操作的错误常量
- 修复健康检查和工具调用中的状态码比较逻辑
- 添加AIToolAuditKey用于工具审计键名

feat(ai-agent): 扩展AI代理常量定义

- 新增AIToolAuditKey用于工具审计字段
- 添加OpenClaw相关的错误常量如OPENCLAW_CHAT_PROVIDER_REQUIRED等
- 添加UNSUPPORTED_AI_SKILL_TEMPLATE模板字符串

refactor(approvals): 审批模块常量化重构

- 新增ApprovalPayloadKey用于审批载荷字段
- 添加approval_action函数和APPROVAL_ACTION_SEPARATOR分隔符
- 使用常量替换字面量值

feat(audit): 审计模块新增飞书事件动作类型

- 添加FEISHU_WEBHOOK_EVENT和FEISHU_LONG_CONNECTION_EVENT审计动作

refactor(business): 业务模块全面常量化

- 新增BusinessDomain枚举包含所有业务域
- 添加BusinessResponseKey、BusinessPayloadKey等常量类
- 重构DOMAIN_MODELS为frozenset以提高性能
- 添加normalize_domain等辅助函数用于域标准化
- 使用常量替换路由和业务服务中的硬编码字符串
- 添加业务错误常量和字段验证模板

refactor(feishu): 飞书客户端错误处理优化

- 将HTTP状态码替换为FastAPI标准状态常量
- 改进错误处理的一致性

refactor(approvals): 审批服务使用新常量结构

- 使用ApprovalPayloadKey常量重构载荷字段
- 使用approval_action函数统一动作命名格式
- 优化高风险域判断逻辑
```
This commit is contained in:
2026-07-06 15:56:43 +08:00
parent 514b14d390
commit fbd0aaa9e4
34 changed files with 1153 additions and 525 deletions

View File

@@ -52,6 +52,23 @@ class LifecycleResponseKey(StrEnum):
PROJECT_LIFECYCLE_REPORT = "project_lifecycle_report"
class ReportResponseKey(StrEnum):
REPORT = "report"
DATA = "data"
TITLE = "title"
REPORT_TYPE = "report_type"
PERIOD_START = "period_start"
PERIOD_END = "period_end"
WORK_DATE = "work_date"
TOTAL = "total"
ABNORMAL_TOTAL = "abnormal_total"
STATUS_COUNTS = "status_counts"
LINES = "lines"
CONTENT = "content"
METRICS = "metrics"
RISK_SUMMARY = "risk_summary"
class LifecycleAttentionKey(StrEnum):
DELAYED_PROJECTS = "delayed_projects"
OVER_BUDGET_PROJECTS = "over_budget_projects"
@@ -109,6 +126,18 @@ class MetricKey(StrEnum):
LEVEL = "level"
class WorkReportMetricKey(StrEnum):
PROJECTS_TOTAL = "projects_total"
ACTIVE_PROJECTS = "active_projects"
TASKS_TOTAL = "tasks_total"
TASKS_COMPLETED = "tasks_completed"
TASKS_OVERDUE = "tasks_overdue"
PROCUREMENTS_PENDING = "procurements_pending"
EXPENSES_PENDING = "expenses_pending"
ATTENDANCE_TOTAL = "attendance_total"
OPEN_RISK_EVENTS = "open_risk_events"
class HealthLevel(StrEnum):
HEALTHY = "healthy"
ATTENTION = "attention"
@@ -133,3 +162,25 @@ class ReportText(StrEnum):
RECOMMEND_STABLE = (
"当前生命周期指标稳定,建议继续保持周度复盘和风险事件归档。"
)
LIFECYCLE_RISK_SCORE_WEIGHTS = {
MetricKey.OVERDUE_TASKS: 1,
MetricKey.DELAYED_PROJECTS: 3,
MetricKey.OVER_BUDGET_PROJECTS: 4,
MetricKey.EXTERNAL_OPEN_EVENTS: 2,
MetricKey.EXTERNAL_HIGH_EVENTS: 3,
}
HEALTH_PENALTY_WEIGHTS = {
MetricKey.OVERDUE_TASKS: 3,
MetricKey.DELAYED_PROJECTS: 8,
MetricKey.OVER_BUDGET_PROJECTS: 10,
MetricKey.EXTERNAL_HIGH_EVENTS: 8,
MetricKey.BUDGET_USAGE_RATE: 0.4,
MetricKey.COMPLETION_RATE: 0.1,
MetricKey.BLACKLISTED: 10,
}
HEALTH_SCORE_MAX = 100
HEALTH_SCORE_MIN = 0
HEALTHY_SCORE_THRESHOLD = 80
ATTENTION_SCORE_THRESHOLD = 60

View File

@@ -35,17 +35,26 @@ from app.modules.business.models import (
from app.modules.business.service import serialize_model
from app.modules.feishu.service import FeishuService
from app.modules.reports.constants import (
ATTENTION_SCORE_THRESHOLD,
HEALTH_PENALTY_WEIGHTS,
HEALTH_SCORE_MAX,
HEALTH_SCORE_MIN,
HEALTHY_SCORE_THRESHOLD,
LIFECYCLE_RISK_SCORE_WEIGHTS,
HealthLevel,
LifecycleAttentionKey,
LifecycleFilterKey,
LifecycleResponseKey,
LifecycleSection,
MetricKey,
ReportResponseKey,
ReportStatus,
ReportText,
ReportTitle,
ReportType,
WorkReportMetricKey,
)
from app.modules.risk.constants import RiskSummaryKey, risk_level_for_score
from app.modules.risk.service import RiskService
@@ -164,18 +173,22 @@ class ReportService:
f"- 待处理费用:{expense_pending}",
f"- 当前账户总余额:{_money(fund_total)}",
(
f"- 今日打卡记录:{attendance['total']}"
f"异常:{attendance['abnormal_total']}"
f"- 今日打卡记录:{attendance[ReportResponseKey.TOTAL]}"
f"异常:{attendance[ReportResponseKey.ABNORMAL_TOTAL]}"
),
f"- 逾期任务:{len(risk_summary['overdue_tasks'])}",
f"- 延期项目:{len(risk_summary['delayed_projects'])}",
f"- 超预算项目:{len(risk_summary['over_budget_projects'])}",
f"- 资金风险账户:{len(risk_summary['fund_risks'])}",
f"- 供应商风险:{len(risk_summary['supplier_risks'])}",
f"- 打开风险事件:{len(risk_summary['open_events'])}",
f"- 综合风险等级:{risk_summary['risk_level']}",
f"- 逾期任务:{len(risk_summary[RiskSummaryKey.OVERDUE_TASKS])}",
f"- 延期项目:{len(risk_summary[RiskSummaryKey.DELAYED_PROJECTS])}",
f"- 超预算项目:{len(risk_summary[RiskSummaryKey.OVER_BUDGET_PROJECTS])}",
f"- 资金风险账户:{len(risk_summary[RiskSummaryKey.FUND_RISKS])}",
f"- 供应商风险:{len(risk_summary[RiskSummaryKey.SUPPLIER_RISKS])}",
f"- 打开风险事件:{len(risk_summary[RiskSummaryKey.OPEN_EVENTS])}",
f"- 综合风险等级:{risk_summary[RiskSummaryKey.RISK_LEVEL]}",
]
return {"title": "每日经营晨报", "lines": lines, "content": "\n".join(lines)}
return {
ReportResponseKey.TITLE: ReportTitle.DAILY_BRIEF,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}
def project_weekly(self) -> dict:
active = self._count(
@@ -199,7 +212,11 @@ class ReportService:
)
for item in over_budget[:10]:
lines.append(f" - 超预算:{item.get('code')} {item.get('name')}")
return {"title": "项目周报", "lines": lines, "content": "\n".join(lines)}
return {
ReportResponseKey.TITLE: ReportTitle.PROJECT_WEEKLY,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}
def project_lifecycle_report(
self,
@@ -559,20 +576,14 @@ class ReportService:
*risk_conditions,
)
risk_score = (
overdue_tasks * 1
+ delayed_projects * 3
+ over_budget_projects * 4
+ external_open_events * 2
+ external_high_events * 3
overdue_tasks * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.OVERDUE_TASKS]
+ delayed_projects * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.DELAYED_PROJECTS]
+ over_budget_projects * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.OVER_BUDGET_PROJECTS]
+ external_open_events * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.EXTERNAL_OPEN_EVENTS]
+ external_high_events * LIFECYCLE_RISK_SCORE_WEIGHTS[MetricKey.EXTERNAL_HIGH_EVENTS]
)
if risk_score >= 15:
level = RiskLevel.HIGH
elif risk_score >= 5:
level = RiskLevel.MEDIUM
else:
level = RiskLevel.LOW
return {
MetricKey.RISK_LEVEL: level,
MetricKey.RISK_LEVEL: risk_level_for_score(risk_score),
MetricKey.RISK_SCORE: risk_score,
MetricKey.OVERDUE_TASKS: overdue_tasks,
MetricKey.DELAYED_PROJECTS: delayed_projects,
@@ -602,19 +613,32 @@ class ReportService:
include_global_risk: bool,
) -> dict[str, Any]:
penalty = (
risks[MetricKey.OVERDUE_TASKS] * 3
+ risks[MetricKey.DELAYED_PROJECTS] * 8
+ risks[MetricKey.OVER_BUDGET_PROJECTS] * 10
+ risks[MetricKey.EXTERNAL_HIGH_EVENTS] * 8
+ max(0, projects[MetricKey.BUDGET_USAGE_RATE] - 100) * 0.4
+ (100 - tasks[MetricKey.COMPLETION_RATE]) * 0.1
risks[MetricKey.OVERDUE_TASKS] * HEALTH_PENALTY_WEIGHTS[MetricKey.OVERDUE_TASKS]
+ risks[MetricKey.DELAYED_PROJECTS]
* HEALTH_PENALTY_WEIGHTS[MetricKey.DELAYED_PROJECTS]
+ risks[MetricKey.OVER_BUDGET_PROJECTS]
* HEALTH_PENALTY_WEIGHTS[MetricKey.OVER_BUDGET_PROJECTS]
+ risks[MetricKey.EXTERNAL_HIGH_EVENTS]
* HEALTH_PENALTY_WEIGHTS[MetricKey.EXTERNAL_HIGH_EVENTS]
+ max(
HEALTH_SCORE_MIN,
projects[MetricKey.BUDGET_USAGE_RATE] - HEALTH_SCORE_MAX,
)
* HEALTH_PENALTY_WEIGHTS[MetricKey.BUDGET_USAGE_RATE]
+ (HEALTH_SCORE_MAX - tasks[MetricKey.COMPLETION_RATE])
* HEALTH_PENALTY_WEIGHTS[MetricKey.COMPLETION_RATE]
)
if include_global_risk:
penalty += suppliers[MetricKey.BLACKLISTED] * 10
score = max(0, min(100, round(100 - penalty, 2)))
if score >= 80:
penalty += suppliers[MetricKey.BLACKLISTED] * HEALTH_PENALTY_WEIGHTS[
MetricKey.BLACKLISTED
]
score = max(
HEALTH_SCORE_MIN,
min(HEALTH_SCORE_MAX, round(HEALTH_SCORE_MAX - penalty, 2)),
)
if score >= HEALTHY_SCORE_THRESHOLD:
level = HealthLevel.HEALTHY
elif score >= 60:
elif score >= ATTENTION_SCORE_THRESHOLD:
level = HealthLevel.ATTENTION
else:
level = HealthLevel.CRITICAL
@@ -801,13 +825,13 @@ class ReportService:
for status, count in sorted(status_counts.items()):
lines.append(f"- {status}{count}")
return {
"title": "打卡汇总",
"work_date": target_date.isoformat(),
"total": total,
"abnormal_total": abnormal_total,
"status_counts": status_counts,
"lines": lines,
"content": "\n".join(lines),
ReportResponseKey.TITLE: ReportTitle.ATTENDANCE_SUMMARY,
ReportResponseKey.WORK_DATE: target_date.isoformat(),
ReportResponseKey.TOTAL: total,
ReportResponseKey.ABNORMAL_TOTAL: abnormal_total,
ReportResponseKey.STATUS_COUNTS: status_counts,
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
}
def generate_work_report(
@@ -833,14 +857,14 @@ class ReportService:
)
lines = self._work_report_lines(title, start, end, metrics, risk_summary)
report = {
"title": title,
"report_type": report_type,
"period_start": start.isoformat(),
"period_end": end.isoformat(),
"lines": lines,
"content": "\n".join(lines),
"metrics": metrics,
"risk_summary": risk_summary,
ReportResponseKey.TITLE: title,
ReportResponseKey.REPORT_TYPE: report_type,
ReportResponseKey.PERIOD_START: start.isoformat(),
ReportResponseKey.PERIOD_END: end.isoformat(),
ReportResponseKey.LINES: lines,
ReportResponseKey.CONTENT: "\n".join(lines),
ReportResponseKey.METRICS: metrics,
ReportResponseKey.RISK_SUMMARY: risk_summary,
}
record_data = None
@@ -854,7 +878,7 @@ class ReportService:
project_code=project_code,
period_start=start,
period_end=end,
content=report["content"],
content=report[ReportResponseKey.CONTENT],
metrics=metrics,
risk_summary=risk_summary,
)
@@ -873,7 +897,7 @@ class ReportService:
)
)
return {"report": report, "data": record_data}
return {ReportResponseKey.REPORT: report, ReportResponseKey.DATA: record_data}
def _resolve_period(
self,
@@ -932,19 +956,25 @@ class ReportService:
*task_filters,
)
return {
"projects_total": self._count(Project, *project_filters),
"active_projects": self._count(
WorkReportMetricKey.PROJECTS_TOTAL: self._count(Project, *project_filters),
WorkReportMetricKey.ACTIVE_PROJECTS: self._count(
Project,
Project.status.notin_(PROJECT_CLOSED_STATUSES),
*project_filters,
),
"tasks_total": self._count(WorkTask, *task_filters),
"tasks_completed": completed_tasks,
"tasks_overdue": overdue_tasks,
"procurements_pending": self._count(Procurement, *procurement_filters),
"expenses_pending": self._count(Expense, *expense_filters),
"attendance_total": self._count(AttendanceRecord, *attendance_filters),
"open_risk_events": self._count(RiskEvent, *risk_filters),
WorkReportMetricKey.TASKS_TOTAL: self._count(WorkTask, *task_filters),
WorkReportMetricKey.TASKS_COMPLETED: completed_tasks,
WorkReportMetricKey.TASKS_OVERDUE: overdue_tasks,
WorkReportMetricKey.PROCUREMENTS_PENDING: self._count(
Procurement,
*procurement_filters,
),
WorkReportMetricKey.EXPENSES_PENDING: self._count(Expense, *expense_filters),
WorkReportMetricKey.ATTENDANCE_TOTAL: self._count(
AttendanceRecord,
*attendance_filters,
),
WorkReportMetricKey.OPEN_RISK_EVENTS: self._count(RiskEvent, *risk_filters),
}
def _work_report_lines(
@@ -958,14 +988,20 @@ class ReportService:
return [
f"- 报告:{title}",
f"- 周期:{start.isoformat()}{end.isoformat()}",
f"- 项目:总数 {metrics['projects_total']},活跃 {metrics['active_projects']}",
f"- 任务:总数 {metrics['tasks_total']},完成 {metrics['tasks_completed']}",
f"- 逾期任务:{metrics['tasks_overdue']}",
f"- 待处理采购:{metrics['procurements_pending']}",
f"- 待处理费用:{metrics['expenses_pending']}",
f"- 打卡记录:{metrics['attendance_total']}",
f"- 打开风险事件:{metrics['open_risk_events']}",
f"- 综合风险等级:{risk_summary['risk_level']}",
(
f"- 项目:总数 {metrics[WorkReportMetricKey.PROJECTS_TOTAL]}"
f"活跃 {metrics[WorkReportMetricKey.ACTIVE_PROJECTS]}"
),
(
f"- 任务:总数 {metrics[WorkReportMetricKey.TASKS_TOTAL]}"
f"完成 {metrics[WorkReportMetricKey.TASKS_COMPLETED]}"
),
f"- 逾期任务:{metrics[WorkReportMetricKey.TASKS_OVERDUE]}",
f"- 待处理采购:{metrics[WorkReportMetricKey.PROCUREMENTS_PENDING]}",
f"- 待处理费用:{metrics[WorkReportMetricKey.EXPENSES_PENDING]}",
f"- 打卡记录:{metrics[WorkReportMetricKey.ATTENDANCE_TOTAL]}",
f"- 打开风险事件:{metrics[WorkReportMetricKey.OPEN_RISK_EVENTS]}",
f"- 综合风险等级:{risk_summary[RiskSummaryKey.RISK_LEVEL]}",
]
def push_report(
@@ -975,5 +1011,8 @@ class ReportService:
receive_id_type: str,
actor: str,
) -> dict:
card = FeishuService.build_basic_card(report["title"], report["lines"])
card = FeishuService.build_basic_card(
report[ReportResponseKey.TITLE],
report[ReportResponseKey.LINES],
)
return FeishuService(self.db).send_card(card, receive_id, receive_id_type, actor)