Prechádzať zdrojové kódy

feat(expense): 员工/部门变动联动 - scope_sync新增员工加入/离开部门时额度同步

- 新增 sync_employee_add_to_department_institutions / sync_employee_remove 函数
- 扫描所有 department 模式的制度,匹配员工部门后创建/删除本地额度记录
- employee_handler 新增/激活/调部门时自动触发联动
alphah 2 týždňov pred
rodič
commit
0a226559d9

+ 119 - 2
backend/app/plugin/module_payment/expense/institution/scope_sync.py

@@ -1,7 +1,8 @@
 """
 费控制度成员联动同步工具
 
-用于部门停用/员工解约时自动移除相关制度中的成员引用。
+部门停用/员工解约时自动移除相关制度中的成员引用。
+员工调部门/部门新增员工时自动创建本地额度记录。
 """
 
 from app.api.v1.module_system.auth.schema import AuthSchema
@@ -10,6 +11,123 @@ from app.core.logger import log
 from app.plugin.module_payment.expense.institution.crud import InstitutionCRUD
 
 
+async def _sync_employee_quota(
+    auth: AuthSchema, enterprise_id: str, employee_id: str, department_ids: list[str], is_add: bool
+) -> None:
+    """根据员工所属部门,同步本地额度记录
+
+    扫描所有按部门模式的制度,如果该制度引用了员工所属部门,
+    则为员工创建(或删除)本地 pay_expense_quota 记录。
+    """
+    if not employee_id:
+        return
+
+    from app.plugin.module_payment.expense.quota.model import QuotaModel
+    from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
+    from sqlalchemy import insert, delete as sa_delete, select
+
+    try:
+        crud = InstitutionCRUD(auth)
+        institutions = await crud.list(
+            search={
+                "enterprise_id": enterprise_id,
+                "status__ne": "INSTITUTION_DELETE",
+                "applicable_scope": "department",
+            },
+            order_by=[{"id": "desc"}],
+        )
+        if not institutions:
+            return
+
+        for inst in institutions:
+            inst_id = inst.institution_id
+            scope_owner_ids_str = getattr(inst, "scope_owner_id_list", None) or getattr(inst, "department_id", None)
+            if not inst_id:
+                continue
+
+            # 判断该制度的部门是否匹配员工部门
+            matched = False
+            if scope_owner_ids_str:
+                import json
+                try:
+                    scope_ids = json.loads(scope_owner_ids_str) if isinstance(scope_owner_ids_str, str) else scope_owner_ids_str
+                except (json.JSONDecodeError, TypeError):
+                    scope_ids = [str(scope_owner_ids_str)] if scope_owner_ids_str else []
+
+                for dept_id in department_ids:
+                    if dept_id in scope_ids:
+                        matched = True
+                        break
+            else:
+                continue
+
+            if not matched:
+                continue
+
+            tenant_id = auth.user.tenant_id if auth.user else 1
+
+            if is_add:
+                # 员工加入部门 → 创建额度记录
+                check = select(QuotaModel).where(
+                    QuotaModel.employee_id == employee_id,
+                    QuotaModel.institution_id == inst_id,
+                )
+                existing = await auth.db.execute(check)
+                if existing.scalar_one_or_none():
+                    continue
+
+                stmt = insert(QuotaModel).values(
+                    employee_id=employee_id,
+                    institution_id=inst_id,
+                    out_biz_no=f"scope_{inst_id}_{employee_id}",
+                    total_amount=0,
+                    available_amount=0,
+                    status=QuotaStatusEnum.QUOTA_PENDING.value,
+                    enterprise_id=enterprise_id,
+                    tenant_id=tenant_id,
+                )
+                await auth.db.execute(stmt)
+                log.info(
+                    f"部门联动 - 新增员工额度: employee_id={employee_id}, "
+                    f"institution_id={inst_id}"
+                )
+            else:
+                # 员工离开部门 → 删除额度记录
+                del_stmt = sa_delete(QuotaModel).where(
+                    QuotaModel.employee_id == employee_id,
+                    QuotaModel.institution_id == inst_id,
+                )
+                await auth.db.execute(del_stmt)
+                log.info(
+                    f"部门联动 - 删除员工额度: employee_id={employee_id}, "
+                    f"institution_id={inst_id}"
+                )
+
+        await auth.db.flush()
+    except Exception as e:
+        log.error(f"部门联动同步额度失败(不影响主体操作): {e}")
+
+
+async def sync_employee_add_to_department_institutions(
+    auth: AuthSchema,
+    enterprise_id: str,
+    employee_id: str,
+    department_ids: list[str],
+) -> None:
+    """员工加入部门时,为引用该部门的制度创建本地额度记录"""
+    await _sync_employee_quota(auth, enterprise_id, employee_id, department_ids, is_add=True)
+
+
+async def sync_employee_remove_from_department_institutions(
+    auth: AuthSchema,
+    enterprise_id: str,
+    employee_id: str,
+    department_ids: list[str],
+) -> None:
+    """员工离开部门时,从引用该部门的制度中删除本地额度记录"""
+    await _sync_employee_quota(auth, enterprise_id, employee_id, department_ids, is_add=False)
+
+
 async def remove_department_from_institution_scopes(
     auth: AuthSchema,
     enterprise_id: str,
@@ -22,7 +140,6 @@ async def remove_department_from_institution_scopes(
     """
     try:
         crud = InstitutionCRUD(auth)
-        # 查所有该企业下的有效制度
         institutions = await crud.list(
             search={"enterprise_id": enterprise_id, "status__ne": "INSTITUTION_DELETE"},
             order_by=[{"id": "desc"}],

+ 32 - 0
backend/app/plugin/module_payment/notification/handlers/employee_handler.py

@@ -73,16 +73,40 @@ class EmployeeHandler(BaseHandler[dict]):
             log.error(f"支付宝通知消息 - 处理员工变更通知消息异常: {e}")
             return False
 
+    async def _get_department_ids(self, data: EmployeeChangeContent) -> list[str]:
+        """从通知数据中提取部门ID列表"""
+        dept_list = data.department_list or []
+        return [
+            (d.get("department_id") or "") for d in dept_list
+            if isinstance(d, dict) and d.get("department_id")
+        ]
+
     async def _handle_add(self, data: EmployeeChangeContent, auth: AuthSchema) -> bool:
         """处理员工新增"""
         log.info(f"员工新增: employee_id={data.employee_id}, enterprise_id={data.enterprise_id}")
         await self.update_employee(data, auth)
+        # 联动:员工新增到部门时,创建额度记录
+        dept_ids = await self._get_department_ids(data)
+        if dept_ids:
+            from app.plugin.module_payment.expense.institution.scope_sync import sync_employee_add_to_department_institutions
+            await sync_employee_add_to_department_institutions(
+                auth=auth, enterprise_id=data.enterprise_id,
+                employee_id=data.employee_id, department_ids=dept_ids,
+            )
         return True
 
     async def _handle_activate(self, data: EmployeeChangeContent, auth: AuthSchema) -> bool:
         """处理员工激活"""
         log.info(f"员工激活: employee_id={data.employee_id}")
         await self.update_employee(data, auth)
+        # 联动:员工激活时,同步到部门制度额度
+        dept_ids = await self._get_department_ids(data)
+        if dept_ids:
+            from app.plugin.module_payment.expense.institution.scope_sync import sync_employee_add_to_department_institutions
+            await sync_employee_add_to_department_institutions(
+                auth=auth, enterprise_id=data.enterprise_id,
+                employee_id=data.employee_id, department_ids=dept_ids,
+            )
         return True
 
     async def _handle_update(self, data: EmployeeChangeContent, auth: AuthSchema) -> bool:
@@ -95,6 +119,14 @@ class EmployeeHandler(BaseHandler[dict]):
         """处理部门修改"""
         log.info(f"部门修改: employee_id={data.employee_id}")
         await self.update_employee(data, auth)
+        # 联动:员工调部门时,重新同步额度
+        dept_ids = await self._get_department_ids(data)
+        if dept_ids:
+            from app.plugin.module_payment.expense.institution.scope_sync import sync_employee_add_to_department_institutions
+            await sync_employee_add_to_department_institutions(
+                auth=auth, enterprise_id=data.enterprise_id,
+                employee_id=data.employee_id, department_ids=dept_ids,
+            )
         return True
 
     async def _handle_role_change(self, data: EmployeeChangeContent, auth: AuthSchema) -> bool: