feat: 新增员工行为画像算法与费用风险标签体系

后端新增员工行为画像算法模块,支持标签规则引擎和评分计算,
完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流
和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费
用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台
样式,优化文档中心和归档中心交互,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-28 12:09:49 +08:00
parent 04cd6d0f81
commit 8a4a777be7
96 changed files with 9835 additions and 704 deletions

View File

@@ -4,7 +4,7 @@ from collections import Counter
from datetime import UTC, date, datetime
from typing import Any
from sqlalchemy import inspect, select, text
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.config import get_settings
@@ -28,6 +28,8 @@ from app.schemas.employee import (
EmployeeUpdate,
)
from app.services.employee_import import EmployeeImportCoordinator
from app.services.employee_bank_info import apply_default_bank_info
from app.services.employee_schema import ensure_employee_schema
from app.services.employee_serialization import serialize_employee
from app.services.employee_spreadsheet import build_import_template_bytes
from app.services.employee_seed import (
@@ -86,12 +88,13 @@ class EmployeeService:
def ensure_directory_ready(self) -> None:
try:
Base.metadata.create_all(bind=self.db.get_bind())
self._ensure_employee_schema()
ensure_employee_schema(self.db)
self._prune_extra_seed_employees()
self._seed_roles()
self._seed_organization_units()
self._seed_employees()
self._normalize_legacy_employee_departments()
self._backfill_employee_bank_info()
self.db.commit()
except Exception:
self.db.rollback()
@@ -191,12 +194,16 @@ class EmployeeService:
grade=payload.grade,
cost_center=payload.cost_center,
finance_owner_name=payload.finance_owner_name,
bank_name=normalize_optional_text(payload.bank_name),
bank_account_no=normalize_optional_text(payload.bank_account_no),
bank_account_name=normalize_optional_text(payload.bank_account_name),
employment_status=payload.employment_status,
sync_state=payload.sync_state,
spotlight=payload.spotlight,
password_hash=hash_password(DEFAULT_EMPLOYEE_PASSWORD),
last_sync_at=datetime.now(UTC),
)
apply_default_bank_info(employee)
if payload.organization_unit_code:
organization_code = normalize_organization_unit_code(payload.organization_unit_code)
@@ -305,6 +312,24 @@ class EmployeeService:
employee.finance_owner_name = finance_owner_name
changed_fields.append("财务归口")
if "bank_account_name" in payload.model_fields_set:
bank_account_name = normalize_optional_text(payload.bank_account_name)
if bank_account_name != employee.bank_account_name:
employee.bank_account_name = bank_account_name
changed_fields.append("银行户名")
if "bank_name" in payload.model_fields_set:
bank_name = normalize_optional_text(payload.bank_name)
if bank_name != employee.bank_name:
employee.bank_name = bank_name
changed_fields.append("开户行")
if "bank_account_no" in payload.model_fields_set:
bank_account_no = normalize_optional_text(payload.bank_account_no)
if bank_account_no != employee.bank_account_no:
employee.bank_account_no = bank_account_no
changed_fields.append("银行账号")
if "organization_unit_code" in payload.model_fields_set:
organization_code = normalize_organization_unit_code(
normalize_optional_text(payload.organization_unit_code)
@@ -581,6 +606,9 @@ class EmployeeService:
grade=definition.get("grade", "P3"),
cost_center=definition.get("cost_center"),
finance_owner_name=definition.get("finance_owner_name"),
bank_name=definition.get("bank_name"),
bank_account_no=definition.get("bank_account_no"),
bank_account_name=definition.get("bank_account_name"),
employment_status=definition.get("employment_status", "在职"),
sync_state=definition.get("sync_state", "已同步"),
spotlight=bool(definition.get("spotlight")),
@@ -606,6 +634,8 @@ class EmployeeService:
if not employee.password_hash:
employee.password_hash = hash_password(DEFAULT_EMPLOYEE_PASSWORD)
apply_default_bank_info(employee)
if not employee.roles:
employee.roles = self._sorted_roles(
[
@@ -655,6 +685,9 @@ class EmployeeService:
"location",
"cost_center",
"finance_owner_name",
"bank_name",
"bank_account_no",
"bank_account_name",
"employment_status",
"sync_state",
):
@@ -673,6 +706,8 @@ class EmployeeService:
if not employee.password_hash:
employee.password_hash = hash_password(DEFAULT_EMPLOYEE_PASSWORD)
apply_default_bank_info(employee)
role_codes = [item for item in definition.get("role_codes", []) if item in roles_by_code]
if role_codes:
merged_roles = {role.role_code: role for role in employee.roles}
@@ -691,19 +726,9 @@ class EmployeeService:
if employee is not None:
self.db.delete(employee)
def _ensure_employee_schema(self) -> None:
bind = self.db.get_bind()
inspector = inspect(bind)
if "employees" not in inspector.get_table_names():
return
column_names = {column["name"] for column in inspector.get_columns("employees")}
if "password_hash" not in column_names:
self.db.execute(text("ALTER TABLE employees ADD COLUMN password_hash VARCHAR(255)"))
if "compliance_score" not in column_names:
self.db.execute(
text("ALTER TABLE employees ADD COLUMN compliance_score INTEGER DEFAULT 100 NOT NULL")
)
def _backfill_employee_bank_info(self) -> None:
for employee in self.repository.list():
apply_default_bank_info(employee)
self.db.flush()
def _seed_employee_history(self, employee: Employee, definition: dict[str, Any]) -> None: