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

@@ -0,0 +1,66 @@
from enum import StrEnum
from app.modules.business.constants import RiskLevel
class RiskSummaryKey(StrEnum):
RISK_LEVEL = "risk_level"
RISK_SCORE = "risk_score"
OVERDUE_TASKS = "overdue_tasks"
DELAYED_PROJECTS = "delayed_projects"
OVER_BUDGET_PROJECTS = "over_budget_projects"
FUND_RISKS = "fund_risks"
SUPPLIER_RISKS = "supplier_risks"
OPEN_EVENTS = "open_events"
class RiskGenerationResultKey(StrEnum):
CREATED = "created"
UPDATED = "updated"
SKIPPED = "skipped"
ITEMS = "items"
ACTION = "action"
RISK_EVENT = "risk_event"
class RiskGenerationAction(StrEnum):
CREATED = "created"
UPDATED = "updated"
SKIPPED = "skipped"
class RiskEventPayloadKey(StrEnum):
CODE = "code"
TITLE = "title"
RISK_TYPE = "risk_type"
RISK_LEVEL = "risk_level"
STATUS = "status"
SOURCE_DOMAIN = "source_domain"
SOURCE_RECORD_ID = "source_record_id"
PROJECT_CODE = "project_code"
OWNER = "owner"
DUE_DATE = "due_date"
DETECTED_AT = "detected_at"
DESCRIPTION = "description"
MITIGATION = "mitigation"
EVIDENCE = "evidence"
RISK_SCORE_WEIGHTS = {
RiskSummaryKey.OVERDUE_TASKS: 1,
RiskSummaryKey.DELAYED_PROJECTS: 3,
RiskSummaryKey.OVER_BUDGET_PROJECTS: 4,
RiskSummaryKey.FUND_RISKS: 5,
RiskSummaryKey.SUPPLIER_RISKS: 3,
RiskSummaryKey.OPEN_EVENTS: 2,
}
RISK_LEVEL_HIGH_THRESHOLD = 15
RISK_LEVEL_MEDIUM_THRESHOLD = 5
def risk_level_for_score(score: int) -> RiskLevel:
if score >= RISK_LEVEL_HIGH_THRESHOLD:
return RiskLevel.HIGH
if score >= RISK_LEVEL_MEDIUM_THRESHOLD:
return RiskLevel.MEDIUM
return RiskLevel.LOW

View File

@@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import ApiPrincipal, require_api_key
from app.modules.risk.constants import RiskGenerationResultKey
from app.modules.risk.service import RiskService
router = APIRouter(dependencies=[Depends(require_api_key)])
@@ -15,27 +16,27 @@ def risk_summary(db: Session = Depends(get_db)) -> dict:
@router.get("/overdue-tasks")
def overdue_tasks(db: Session = Depends(get_db)) -> dict:
return {"items": RiskService(db).overdue_tasks()}
return {RiskGenerationResultKey.ITEMS: RiskService(db).overdue_tasks()}
@router.get("/delayed-projects")
def delayed_projects(db: Session = Depends(get_db)) -> dict:
return {"items": RiskService(db).delayed_projects()}
return {RiskGenerationResultKey.ITEMS: RiskService(db).delayed_projects()}
@router.get("/over-budget-projects")
def over_budget_projects(db: Session = Depends(get_db)) -> dict:
return {"items": RiskService(db).over_budget_projects()}
return {RiskGenerationResultKey.ITEMS: RiskService(db).over_budget_projects()}
@router.get("/funds")
def fund_risks(db: Session = Depends(get_db)) -> dict:
return {"items": RiskService(db).fund_risks()}
return {RiskGenerationResultKey.ITEMS: RiskService(db).fund_risks()}
@router.get("/suppliers")
def supplier_risks(db: Session = Depends(get_db)) -> dict:
return {"items": RiskService(db).supplier_risks()}
return {RiskGenerationResultKey.ITEMS: RiskService(db).supplier_risks()}
@router.get("/events")
@@ -44,7 +45,12 @@ def risk_events(
status: str | None = None,
db: Session = Depends(get_db),
) -> dict:
return {"items": RiskService(db).list_events(limit=limit, status_filter=status)}
return {
RiskGenerationResultKey.ITEMS: RiskService(db).list_events(
limit=limit,
status_filter=status,
)
}
@router.post("/events/generate")

View File

@@ -29,6 +29,14 @@ from app.modules.business.constants import (
)
from app.modules.business.models import FundAccount, Project, RiskEvent, Supplier, WorkTask
from app.modules.business.service import serialize_model
from app.modules.risk.constants import (
RISK_SCORE_WEIGHTS,
RiskGenerationAction,
RiskGenerationResultKey,
RiskEventPayloadKey,
RiskSummaryKey,
risk_level_for_score,
)
class RiskService:
@@ -97,31 +105,26 @@ class RiskService:
external_open_events = [
item
for item in open_events
if item.get("risk_type") not in GENERATED_RISK_EVENT_TYPES
if item.get(RiskEventPayloadKey.RISK_TYPE) not in GENERATED_RISK_EVENT_TYPES
]
risk_score = (
len(overdue_tasks) * 1
+ len(delayed_projects) * 3
+ len(over_budget_projects) * 4
+ len(fund_risks) * 5
+ len(supplier_risks) * 3
+ len(external_open_events) * 2
len(overdue_tasks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OVERDUE_TASKS]
+ len(delayed_projects) * RISK_SCORE_WEIGHTS[RiskSummaryKey.DELAYED_PROJECTS]
+ len(over_budget_projects)
* RISK_SCORE_WEIGHTS[RiskSummaryKey.OVER_BUDGET_PROJECTS]
+ len(fund_risks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.FUND_RISKS]
+ len(supplier_risks) * RISK_SCORE_WEIGHTS[RiskSummaryKey.SUPPLIER_RISKS]
+ len(external_open_events) * RISK_SCORE_WEIGHTS[RiskSummaryKey.OPEN_EVENTS]
)
if risk_score >= 15:
level = RiskLevel.HIGH
elif risk_score >= 5:
level = RiskLevel.MEDIUM
else:
level = RiskLevel.LOW
return {
"risk_level": level,
"risk_score": Decimal(risk_score),
"overdue_tasks": overdue_tasks,
"delayed_projects": delayed_projects,
"over_budget_projects": over_budget_projects,
"fund_risks": fund_risks,
"supplier_risks": supplier_risks,
"open_events": open_events,
RiskSummaryKey.RISK_LEVEL: risk_level_for_score(risk_score),
RiskSummaryKey.RISK_SCORE: Decimal(risk_score),
RiskSummaryKey.OVERDUE_TASKS: overdue_tasks,
RiskSummaryKey.DELAYED_PROJECTS: delayed_projects,
RiskSummaryKey.OVER_BUDGET_PROJECTS: over_budget_projects,
RiskSummaryKey.FUND_RISKS: fund_risks,
RiskSummaryKey.SUPPLIER_RISKS: supplier_risks,
RiskSummaryKey.OPEN_EVENTS: open_events,
}
def generate_events(self, actor: str = ActorValue.API) -> dict[str, Any]:
@@ -135,25 +138,35 @@ class RiskService:
for payload in payloads:
record = self.db.execute(
select(RiskEvent).where(RiskEvent.code == payload["code"])
select(RiskEvent).where(RiskEvent.code == payload[RiskEventPayloadKey.CODE])
).scalar_one_or_none()
if record is None:
record = RiskEvent(**payload)
self.db.add(record)
self.db.flush()
created += 1
action = "created"
action = RiskGenerationAction.CREATED
elif record.status in CLOSED_RISK_STATUSES:
skipped += 1
items.append({"action": "skipped", "risk_event": serialize_model(record)})
items.append(
{
RiskGenerationResultKey.ACTION: RiskGenerationAction.SKIPPED,
RiskGenerationResultKey.RISK_EVENT: serialize_model(record),
}
)
continue
else:
for key, value in payload.items():
if key != "code":
if key != RiskEventPayloadKey.CODE:
setattr(record, key, value)
updated += 1
action = "updated"
items.append({"action": action, "risk_event": serialize_model(record)})
action = RiskGenerationAction.UPDATED
items.append(
{
RiskGenerationResultKey.ACTION: action,
RiskGenerationResultKey.RISK_EVENT: serialize_model(record),
}
)
self.db.commit()
AuditService(self.db).log(
@@ -164,13 +177,18 @@ class RiskService:
target_type=AuditTargetType.RISK_EVENTS,
risk_level=AuditRiskLevel.MEDIUM,
response_payload={
"created": created,
"updated": updated,
"skipped": skipped,
RiskGenerationResultKey.CREATED: created,
RiskGenerationResultKey.UPDATED: updated,
RiskGenerationResultKey.SKIPPED: skipped,
},
)
)
return {"created": created, "updated": updated, "skipped": skipped, "items": items}
return {
RiskGenerationResultKey.CREATED: created,
RiskGenerationResultKey.UPDATED: updated,
RiskGenerationResultKey.SKIPPED: skipped,
RiskGenerationResultKey.ITEMS: items,
}
def _build_event_payloads(self) -> list[dict[str, Any]]:
payloads: list[dict[str, Any]] = []
@@ -191,22 +209,22 @@ class RiskService:
for task in self.db.execute(stmt).scalars():
payloads.append(
{
"code": f"RISK-TASK-OVERDUE-{task.id}",
"title": f"任务逾期:{task.title}",
"risk_type": RiskEventType.OVERDUE_TASK,
"risk_level": RiskLevel.MEDIUM,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.TASKS,
"source_record_id": str(task.id),
"project_code": task.project_code,
"owner": task.owner,
"due_date": task.due_date,
"detected_at": utc_now(),
"description": "任务已超过截止日期且未完成。",
"mitigation": (
RiskEventPayloadKey.CODE: f"RISK-TASK-OVERDUE-{task.id}",
RiskEventPayloadKey.TITLE: f"任务逾期:{task.title}",
RiskEventPayloadKey.RISK_TYPE: RiskEventType.OVERDUE_TASK,
RiskEventPayloadKey.RISK_LEVEL: RiskLevel.MEDIUM,
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.TASKS,
RiskEventPayloadKey.SOURCE_RECORD_ID: str(task.id),
RiskEventPayloadKey.PROJECT_CODE: task.project_code,
RiskEventPayloadKey.OWNER: task.owner,
RiskEventPayloadKey.DUE_DATE: task.due_date,
RiskEventPayloadKey.DETECTED_AT: utc_now(),
RiskEventPayloadKey.DESCRIPTION: "任务已超过截止日期且未完成。",
RiskEventPayloadKey.MITIGATION: (
"请负责人更新进度、明确阻塞项并给出新的完成时间。"
),
"evidence": serialize_model(task),
RiskEventPayloadKey.EVIDENCE: serialize_model(task),
}
)
return payloads
@@ -222,22 +240,22 @@ class RiskService:
level = RiskLevel.HIGH if project.progress_percent < 80 else RiskLevel.MEDIUM
payloads.append(
{
"code": f"RISK-PROJECT-DELAY-{project.id}",
"title": f"项目延期:{project.name}",
"risk_type": RiskEventType.DELAYED_PROJECT,
"risk_level": level,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.PROJECTS,
"source_record_id": str(project.id),
"project_code": project.code,
"owner": project.owner,
"due_date": project.due_date,
"detected_at": utc_now(),
"description": "项目已超过计划截止日期且未进入完成状态。",
"mitigation": (
RiskEventPayloadKey.CODE: f"RISK-PROJECT-DELAY-{project.id}",
RiskEventPayloadKey.TITLE: f"项目延期:{project.name}",
RiskEventPayloadKey.RISK_TYPE: RiskEventType.DELAYED_PROJECT,
RiskEventPayloadKey.RISK_LEVEL: level,
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.PROJECTS,
RiskEventPayloadKey.SOURCE_RECORD_ID: str(project.id),
RiskEventPayloadKey.PROJECT_CODE: project.code,
RiskEventPayloadKey.OWNER: project.owner,
RiskEventPayloadKey.DUE_DATE: project.due_date,
RiskEventPayloadKey.DETECTED_AT: utc_now(),
RiskEventPayloadKey.DESCRIPTION: "项目已超过计划截止日期且未进入完成状态。",
RiskEventPayloadKey.MITIGATION: (
"请项目负责人提交延期原因、资源需求和纠偏计划。"
),
"evidence": serialize_model(project),
RiskEventPayloadKey.EVIDENCE: serialize_model(project),
}
)
return payloads
@@ -251,22 +269,22 @@ class RiskService:
for project in self.db.execute(stmt).scalars():
payloads.append(
{
"code": f"RISK-PROJECT-BUDGET-{project.id}",
"title": f"项目超预算:{project.name}",
"risk_type": RiskEventType.OVER_BUDGET_PROJECT,
"risk_level": RiskLevel.HIGH,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.PROJECTS,
"source_record_id": str(project.id),
"project_code": project.code,
"owner": project.owner,
"due_date": project.due_date,
"detected_at": utc_now(),
"description": "项目实际成本已超过预算。",
"mitigation": (
RiskEventPayloadKey.CODE: f"RISK-PROJECT-BUDGET-{project.id}",
RiskEventPayloadKey.TITLE: f"项目超预算:{project.name}",
RiskEventPayloadKey.RISK_TYPE: RiskEventType.OVER_BUDGET_PROJECT,
RiskEventPayloadKey.RISK_LEVEL: RiskLevel.HIGH,
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.PROJECTS,
RiskEventPayloadKey.SOURCE_RECORD_ID: str(project.id),
RiskEventPayloadKey.PROJECT_CODE: project.code,
RiskEventPayloadKey.OWNER: project.owner,
RiskEventPayloadKey.DUE_DATE: project.due_date,
RiskEventPayloadKey.DETECTED_AT: utc_now(),
RiskEventPayloadKey.DESCRIPTION: "项目实际成本已超过预算。",
RiskEventPayloadKey.MITIGATION: (
"请复核预算科目、冻结非必要采购并补充审批依据。"
),
"evidence": serialize_model(project),
RiskEventPayloadKey.EVIDENCE: serialize_model(project),
}
)
return payloads
@@ -277,21 +295,21 @@ class RiskService:
for account in self.db.execute(stmt).scalars():
payloads.append(
{
"code": f"RISK-FUND-{account.id}",
"title": f"资金低于安全线:{account.name}",
"risk_type": RiskEventType.FUND_SAFETY_LINE,
"risk_level": RiskLevel.HIGH,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.FUND_ACCOUNTS,
"source_record_id": str(account.id),
"owner": None,
"detected_at": utc_now(),
"description": "账户当前余额低于设置的安全线。",
"mitigation": (
RiskEventPayloadKey.CODE: f"RISK-FUND-{account.id}",
RiskEventPayloadKey.TITLE: f"资金低于安全线:{account.name}",
RiskEventPayloadKey.RISK_TYPE: RiskEventType.FUND_SAFETY_LINE,
RiskEventPayloadKey.RISK_LEVEL: RiskLevel.HIGH,
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.FUND_ACCOUNTS,
RiskEventPayloadKey.SOURCE_RECORD_ID: str(account.id),
RiskEventPayloadKey.OWNER: None,
RiskEventPayloadKey.DETECTED_AT: utc_now(),
RiskEventPayloadKey.DESCRIPTION: "账户当前余额低于设置的安全线。",
RiskEventPayloadKey.MITIGATION: (
"请财务确认收付款计划,"
"并优先处理关键项目资金安排。"
),
"evidence": serialize_model(account),
RiskEventPayloadKey.EVIDENCE: serialize_model(account),
}
)
return payloads
@@ -310,20 +328,20 @@ class RiskService:
)
payloads.append(
{
"code": f"RISK-SUPPLIER-{supplier.id}",
"title": f"供应商风险:{supplier.name}",
"risk_type": RiskEventType.SUPPLIER_RISK,
"risk_level": level,
"status": StatusValue.OPEN,
"source_domain": BusinessDomain.SUPPLIERS,
"source_record_id": str(supplier.id),
"owner": supplier.contact,
"detected_at": utc_now(),
"description": "供应商风险等级或黑名单状态需要关注。",
"mitigation": (
RiskEventPayloadKey.CODE: f"RISK-SUPPLIER-{supplier.id}",
RiskEventPayloadKey.TITLE: f"供应商风险:{supplier.name}",
RiskEventPayloadKey.RISK_TYPE: RiskEventType.SUPPLIER_RISK,
RiskEventPayloadKey.RISK_LEVEL: level,
RiskEventPayloadKey.STATUS: StatusValue.OPEN,
RiskEventPayloadKey.SOURCE_DOMAIN: BusinessDomain.SUPPLIERS,
RiskEventPayloadKey.SOURCE_RECORD_ID: str(supplier.id),
RiskEventPayloadKey.OWNER: supplier.contact,
RiskEventPayloadKey.DETECTED_AT: utc_now(),
RiskEventPayloadKey.DESCRIPTION: "供应商风险等级或黑名单状态需要关注。",
RiskEventPayloadKey.MITIGATION: (
"请采购负责人复核供应商准入、履约和替代方案。"
),
"evidence": serialize_model(supplier),
RiskEventPayloadKey.EVIDENCE: serialize_model(supplier),
}
)
return payloads