feat: 新增员工行为画像算法与费用风险标签体系
后端新增员工行为画像算法模块,支持标签规则引擎和评分计算, 完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流 和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费 用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台 样式,优化文档中心和归档中心交互,补充单元测试。
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user