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

@@ -0,0 +1,358 @@
"""Add Intasect project and personnel lifecycle foundation.
Revision ID: 202607120001
Revises: 202607090001
Create Date: 2026-07-12
"""
from alembic import op
import sqlalchemy as sa
revision = "202607120001"
down_revision = "202607090001"
branch_labels = None
depends_on = None
def _indexes(table_name: str, columns: list[tuple[str, bool]]) -> None:
for column_name, unique in columns:
op.create_index(
op.f(f"ix_{table_name}_{column_name}"),
table_name,
[column_name],
unique=unique,
)
def _add_columns(table_name: str, columns: list[sa.Column]) -> None:
for column in columns:
op.add_column(table_name, column)
def upgrade() -> None:
op.create_table(
"employees",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("code", sa.String(length=64), nullable=False),
sa.Column("name", sa.String(length=128), nullable=False),
sa.Column("department_code", sa.String(length=64), nullable=True),
sa.Column("department_name", sa.String(length=128), nullable=True),
sa.Column("title", sa.String(length=128), nullable=True),
sa.Column("employment_status", sa.String(length=32), nullable=False),
sa.Column("hired_at", sa.DateTime(), nullable=True),
sa.Column("ding_user_id", sa.String(length=64), nullable=True),
sa.Column("source_system", sa.String(length=64), nullable=False),
sa.Column("external_id", sa.String(length=128), nullable=False),
sa.Column("source_created_at", sa.DateTime(), nullable=True),
sa.Column("source_updated_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
_indexes(
"employees",
[
("code", True),
("name", False),
("department_code", False),
("department_name", False),
("employment_status", False),
("ding_user_id", False),
("external_id", False),
("source_updated_at", False),
("last_seen_at", False),
("is_active", False),
],
)
op.create_table(
"project_members",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("code", sa.String(length=64), nullable=False),
sa.Column("project_code", sa.String(length=64), nullable=False),
sa.Column("employee_code", sa.String(length=64), nullable=False),
sa.Column("role_name", sa.String(length=128), nullable=True),
sa.Column("is_leader", sa.Boolean(), nullable=False),
sa.Column("is_resident", sa.Boolean(), nullable=False),
sa.Column("work_mode", sa.Text(), nullable=True),
sa.Column("planned_days", sa.Integer(), nullable=True),
sa.Column("workload_percent", sa.Integer(), nullable=True),
sa.Column("source_system", sa.String(length=64), nullable=False),
sa.Column("external_id", sa.String(length=128), nullable=False),
sa.Column("source_created_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
_indexes(
"project_members",
[
("code", True),
("project_code", False),
("employee_code", False),
("role_name", False),
("is_leader", False),
("external_id", False),
("last_seen_at", False),
("is_active", False),
],
)
op.create_table(
"project_milestones",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("code", sa.String(length=128), nullable=False),
sa.Column("project_code", sa.String(length=64), nullable=False),
sa.Column("source_stage_id", sa.String(length=64), nullable=False),
sa.Column("stage_name", sa.String(length=128), nullable=False),
sa.Column("plan_start", sa.Date(), nullable=True),
sa.Column("plan_end", sa.Date(), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("is_overdue", sa.Boolean(), nullable=False),
sa.Column("activity_total", sa.Integer(), nullable=False),
sa.Column("activity_completed", sa.Integer(), nullable=False),
sa.Column("source_system", sa.String(length=64), nullable=False),
sa.Column("external_id", sa.String(length=128), nullable=False),
sa.Column("source_updated_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
_indexes(
"project_milestones",
[
("code", True),
("project_code", False),
("source_stage_id", False),
("stage_name", False),
("plan_end", False),
("status", False),
("is_overdue", False),
("external_id", False),
("source_updated_at", False),
("last_seen_at", False),
("is_active", False),
],
)
op.create_table(
"source_sync_cursors",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("dataset", sa.String(length=64), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("watermark_at", sa.DateTime(), nullable=True),
sa.Column("watermark_key", sa.String(length=128), nullable=True),
sa.Column("last_run_code", sa.String(length=64), nullable=True),
sa.Column("last_success_at", sa.DateTime(), nullable=True),
sa.Column("processed_count", sa.Integer(), nullable=False),
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
_indexes(
"source_sync_cursors",
[
("dataset", True),
("status", False),
("last_run_code", False),
("last_success_at", False),
],
)
_add_columns(
"projects",
[
sa.Column("display_code", sa.String(length=64), nullable=True),
sa.Column("department_code", sa.String(length=64), nullable=True),
sa.Column("department_name", sa.String(length=128), nullable=True),
sa.Column("owner_employee_code", sa.String(length=64), nullable=True),
sa.Column("source_stage", sa.String(length=32), nullable=True),
sa.Column("source_stage_label", sa.String(length=128), nullable=True),
sa.Column("source_archived", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("source_created_at", sa.DateTime(), nullable=True),
sa.Column("source_updated_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
],
)
_indexes(
"projects",
[
("display_code", False),
("department_code", False),
("department_name", False),
("owner_employee_code", False),
("source_stage", False),
("source_archived", False),
("source_updated_at", False),
("last_seen_at", False),
("is_active", False),
],
)
_add_columns(
"work_tasks",
[
sa.Column("employee_code", sa.String(length=64), nullable=True),
sa.Column("source_created_at", sa.DateTime(), nullable=True),
sa.Column("source_updated_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
],
)
_indexes(
"work_tasks",
[
("employee_code", False),
("source_updated_at", False),
("last_seen_at", False),
("is_active", False),
],
)
_add_columns(
"attendance_records",
[
sa.Column(
"attendance_scope", sa.String(length=32), nullable=False, server_default="company"
),
sa.Column("source_status", sa.String(length=64), nullable=True),
sa.Column("source_location_status", sa.String(length=64), nullable=True),
sa.Column("source_updated_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
],
)
_indexes(
"attendance_records",
[
("attendance_scope", False),
("source_status", False),
("source_updated_at", False),
("last_seen_at", False),
("is_active", False),
],
)
_add_columns(
"work_reports",
[
sa.Column("employee_code", sa.String(length=64), nullable=True),
sa.Column("external_id", sa.String(length=128), nullable=True),
sa.Column("is_late", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("is_draft", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("source_updated_at", sa.DateTime(), nullable=True),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
],
)
_indexes(
"work_reports",
[
("employee_code", False),
("external_id", False),
("is_late", False),
("is_draft", False),
("source_updated_at", False),
("last_seen_at", False),
("is_active", False),
],
)
op.add_column(
"report_push_runs", sa.Column("idempotency_key", sa.String(length=128), nullable=True)
)
op.create_index(
op.f("ix_report_push_runs_idempotency_key"),
"report_push_runs",
["idempotency_key"],
unique=True,
)
def downgrade() -> None:
op.drop_index(op.f("ix_report_push_runs_idempotency_key"), table_name="report_push_runs")
op.drop_column("report_push_runs", "idempotency_key")
index_columns = {
"work_reports": {
"employee_code",
"external_id",
"is_late",
"is_draft",
"source_updated_at",
"last_seen_at",
"is_active",
},
"attendance_records": {
"attendance_scope",
"source_status",
"source_updated_at",
"last_seen_at",
"is_active",
},
"work_tasks": {"employee_code", "source_updated_at", "last_seen_at", "is_active"},
"projects": {
"display_code",
"department_code",
"department_name",
"owner_employee_code",
"source_stage",
"source_archived",
"source_updated_at",
"last_seen_at",
"is_active",
},
}
for table_name, columns in {
"work_reports": [
"employee_code",
"external_id",
"is_late",
"is_draft",
"source_updated_at",
"last_seen_at",
"is_active",
],
"attendance_records": [
"attendance_scope",
"source_status",
"source_location_status",
"source_updated_at",
"last_seen_at",
"is_active",
],
"work_tasks": [
"employee_code",
"source_created_at",
"source_updated_at",
"last_seen_at",
"is_active",
],
"projects": [
"display_code",
"department_code",
"department_name",
"owner_employee_code",
"source_stage",
"source_stage_label",
"source_archived",
"source_created_at",
"source_updated_at",
"last_seen_at",
"is_active",
],
}.items():
for column_name in reversed(columns):
if column_name in index_columns[table_name]:
op.drop_index(op.f(f"ix_{table_name}_{column_name}"), table_name=table_name)
op.drop_column(table_name, column_name)
for table_name in ["source_sync_cursors", "project_milestones", "project_members", "employees"]:
op.drop_table(table_name)