feat: 添加生命周期报告和AI规则管理功能

- 在Dockerfile中添加pillow依赖包用于图像处理
- 实现生命周期报告调度任务,支持日报和周报两种类型
- 新增TASK_RUN_LIFECYCLE任务常量和相关配置选项
- 扩展AI Agent服务以支持用户规则,并在分析时应用规则
- 添加AI用户规则创建、更新和查询接口
- 增加项目生命周期和财务需求分析技能
- 扩展现有模型以支持更完整的业务数据字段
- 实现飞书图片上传功能用于报告展示
```
This commit is contained in:
2026-07-12 17:44:49 +08:00
parent bf309ecdf7
commit 9cf7c44393
45 changed files with 5036 additions and 49 deletions

View File

@@ -54,6 +54,25 @@ class SourceSystem(StrEnum):
LEGACY_MYSQL = "legacy_mysql"
class CashFlowDirection(StrEnum):
INFLOW = "inflow"
OUTFLOW = "outflow"
class CashFlowType(StrEnum):
CONTRACT_RECEIVABLE = "contract_receivable"
PROJECT_FUND = "project_fund"
class DataQualityStatus(StrEnum):
VALID = "valid"
ORPHAN_CONTRACT = "orphan_contract"
ORPHAN_PROJECT = "orphan_project"
DELETED_PROJECT = "deleted_project"
PAID_AMOUNT_MISSING = "paid_amount_missing"
STATUS_AMOUNT_MISMATCH = "status_amount_mismatch"
class AccountType(StrEnum):
BANK = "bank"

View File

@@ -2,6 +2,14 @@ from app.modules.business.models.attendance import AttendanceRecord
from app.modules.business.models.finance import Expense, FundAccount, Procurement
from app.modules.business.models.governance import PerformanceMetric, Policy, Standard
from app.modules.business.models.legacy import LegacySyncRun
from app.modules.business.models.lifecycle import (
Employee,
ProjectCashFlow,
ProjectContract,
ProjectMember,
ProjectMilestone,
SourceSyncCursor,
)
from app.modules.business.models.projects import Project, WorkTask
from app.modules.business.models.reports import ReportPushRun, WorkReport
from app.modules.business.models.risks import RiskEvent, RiskEventAction
@@ -11,17 +19,23 @@ from app.modules.business.models.suppliers import Supplier
__all__ = [
"AttendanceRecord",
"Expense",
"Employee",
"FundAccount",
"LegacySyncRun",
"PerformanceMetric",
"Policy",
"Procurement",
"Project",
"ProjectCashFlow",
"ProjectContract",
"ProjectMember",
"ProjectMilestone",
"ReportPushRun",
"RiskEvent",
"RiskEventAction",
"Standard",
"Supplier",
"SourceSyncCursor",
"WorkReport",
"WorkTask",
]

View File

@@ -1,6 +1,6 @@
from datetime import date, datetime
from sqlalchemy import Date, DateTime, Integer, String, Text
from sqlalchemy import Boolean, Date, DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
@@ -28,3 +28,9 @@ class AttendanceRecord(Base, TimestampMixin):
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
external_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
attendance_scope: Mapped[str] = mapped_column(String(32), default="company", index=True)
source_status: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
source_location_status: Mapped[str | None] = mapped_column(String(64), nullable=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)

View File

@@ -0,0 +1,135 @@
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Boolean, Date, DateTime, Integer, Numeric, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
from app.modules.business.constants import SourceSystem, StatusValue
from app.modules.business.models.common import TimestampMixin
class Employee(Base, TimestampMixin):
__tablename__ = "employees"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
name: Mapped[str] = mapped_column(String(128), index=True)
department_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
department_name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
title: Mapped[str | None] = mapped_column(String(128), nullable=True)
employment_status: Mapped[str] = mapped_column(
String(32), default=StatusValue.UNKNOWN, index=True
)
hired_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
ding_user_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.LEGACY_MYSQL)
external_id: Mapped[str] = mapped_column(String(128), index=True)
source_created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class ProjectMember(Base, TimestampMixin):
__tablename__ = "project_members"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
project_code: Mapped[str] = mapped_column(String(64), index=True)
employee_code: Mapped[str] = mapped_column(String(64), index=True)
role_name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
is_leader: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
is_resident: Mapped[bool] = mapped_column(Boolean, default=False)
work_mode: Mapped[str | None] = mapped_column(Text, nullable=True)
planned_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
workload_percent: Mapped[int | None] = mapped_column(Integer, nullable=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.LEGACY_MYSQL)
external_id: Mapped[str] = mapped_column(String(128), index=True)
source_created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class ProjectMilestone(Base, TimestampMixin):
__tablename__ = "project_milestones"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(128), unique=True, index=True)
project_code: Mapped[str] = mapped_column(String(64), index=True)
source_stage_id: Mapped[str] = mapped_column(String(64), index=True)
stage_name: Mapped[str] = mapped_column(String(128), index=True)
plan_start: Mapped[date | None] = mapped_column(Date, nullable=True)
plan_end: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
status: Mapped[str] = mapped_column(String(32), default=StatusValue.UNKNOWN, index=True)
is_overdue: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
activity_total: Mapped[int] = mapped_column(Integer, default=0)
activity_completed: Mapped[int] = mapped_column(Integer, default=0)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.LEGACY_MYSQL)
external_id: Mapped[str] = mapped_column(String(128), index=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class ProjectContract(Base, TimestampMixin):
__tablename__ = "project_contracts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
project_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
contract_type_code: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
contract_type_label: Mapped[str | None] = mapped_column(String(128), nullable=True)
amount: Mapped[Decimal] = mapped_column(Numeric(16, 2), default=0)
signed_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
start_date: Mapped[date | None] = mapped_column(Date, nullable=True)
end_date: Mapped[date | None] = mapped_column(Date, nullable=True)
invoice_type_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
data_quality_status: Mapped[str] = mapped_column(String(32), index=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.LEGACY_MYSQL)
external_id: Mapped[str] = mapped_column(String(128), index=True)
source_created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class ProjectCashFlow(Base, TimestampMixin):
__tablename__ = "project_cash_flows"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
project_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
contract_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
flow_type: Mapped[str] = mapped_column(String(32), index=True)
direction: Mapped[str] = mapped_column(String(16), index=True)
category_code: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
category_label: Mapped[str | None] = mapped_column(String(128), nullable=True)
planned_amount: Mapped[Decimal] = mapped_column(Numeric(16, 2), default=0)
actual_amount: Mapped[Decimal | None] = mapped_column(Numeric(16, 2), nullable=True)
planned_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
actual_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
payment_status: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
invoice_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
approval_status: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
confirmation_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
data_quality_status: Mapped[str] = mapped_column(String(32), index=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.LEGACY_MYSQL)
external_id: Mapped[str] = mapped_column(String(128), index=True)
source_created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class SourceSyncCursor(Base, TimestampMixin):
__tablename__ = "source_sync_cursors"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
dataset: Mapped[str] = mapped_column(String(64), unique=True, index=True)
status: Mapped[str] = mapped_column(String(32), default=StatusValue.PENDING, index=True)
watermark_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
watermark_key: Mapped[str | None] = mapped_column(String(128), nullable=True)
last_run_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
last_success_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
processed_count: Mapped[int] = mapped_column(Integer, default=0)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)

View File

@@ -1,7 +1,7 @@
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Date, DateTime, Integer, Numeric, String, Text
from sqlalchemy import Boolean, Date, DateTime, Integer, Numeric, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
@@ -27,11 +27,28 @@ class Project(Base, TimestampMixin):
risk_level: Mapped[str] = mapped_column(String(32), default=RiskLevel.LOW, index=True)
budget_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0)
actual_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), default=0)
source_contract_amount: Mapped[Decimal | None] = mapped_column(
Numeric(16, 2), nullable=True
)
source_project_investment_amount: Mapped[Decimal | None] = mapped_column(
Numeric(16, 2), nullable=True
)
start_date: Mapped[date | None] = mapped_column(Date, nullable=True)
due_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
external_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
display_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
department_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
department_name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
owner_employee_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
source_stage: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
source_stage_label: Mapped[str | None] = mapped_column(String(128), nullable=True)
source_archived: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
source_created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class WorkTask(Base, TimestampMixin):
__tablename__ = "work_tasks"
@@ -49,3 +66,8 @@ class WorkTask(Base, TimestampMixin):
description: Mapped[str | None] = mapped_column(Text, nullable=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
external_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
employee_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
source_created_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)

View File

@@ -1,6 +1,6 @@
from datetime import date, datetime
from sqlalchemy import JSON, Date, DateTime, Integer, String, Text
from sqlalchemy import Boolean, JSON, Date, DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.constants import ActorValue
@@ -30,6 +30,13 @@ class WorkReport(Base, TimestampMixin):
risk_summary: Mapped[dict | None] = mapped_column(JSON, nullable=True)
status: Mapped[str] = mapped_column(String(64), default=StatusValue.GENERATED, index=True)
source_system: Mapped[str] = mapped_column(String(64), default=SourceSystem.INTERNAL)
employee_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
external_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
is_late: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
is_draft: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
class ReportPushRun(Base, TimestampMixin):
__tablename__ = "report_push_runs"
@@ -47,3 +54,4 @@ class ReportPushRun(Base, TimestampMixin):
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
queued_at: Mapped[datetime] = mapped_column(DateTime, default=utc_now, index=True)
sent_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
idempotency_key: Mapped[str | None] = mapped_column(String(128), nullable=True, unique=True, index=True)