Browse Source

fix(expense): 员工离职联动+调部门清理旧额度 - scope_sync 双向同步

- EMPLOYEE_DELETE 触发 remove_employee_from_institution_scopes
- 调部门时同步扫描全部 department 制度:匹配的创建,不匹配的删除
- 适应 dept A→B 场景,旧记录自动清理
alphah 2 weeks ago
parent
commit
f68a7a586b

+ 35 - 47
backend/app/plugin/module_payment/expense/institution/scope_sync.py

@@ -16,8 +16,9 @@ async def _sync_employee_quota(
 ) -> None:
     """根据员工所属部门,同步本地额度记录
 
-    扫描所有按部门模式的制度,如果该制度引用了员工所属部门,
-    则为员工创建(或删除)本地 pay_expense_quota 记录。
+    扫描所有按部门模式的制度:
+    - 匹配的部门 → 确保员工有额度记录(创建缺失的)
+    - 不匹配的部门 → 删除该员工的额度记录(处理离部门场景)
     """
     if not employee_id:
         return
@@ -39,69 +40,56 @@ async def _sync_employee_quota(
         if not institutions:
             return
 
+        tenant_id = auth.user.tenant_id if auth.user else 1
+        dept_id_set = set(department_ids)
+
         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:
+            if not inst_id or not scope_owner_ids_str:
                 continue
 
-            if not matched:
-                continue
+            # 解析制度的部门ID列表
+            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 []
+            scope_ids = [str(s) for s in scope_ids]
 
-            tenant_id = auth.user.tenant_id if auth.user else 1
+            # 判断是否匹配
+            matched = bool(scope_ids and dept_id_set.intersection(scope_ids))
 
-            if is_add:
-                # 员工加入部门 → 创建额度记录
+            if matched:
+                # 匹配 → 确保有额度记录
                 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}"
-                )
+                if not existing.scalar_one_or_none():
+                    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}, institution_id={inst_id}")
             else:
-                # 员工离开部门 → 删除额度记录
+                # 不匹配 → 删除该员工的额度记录
+                if not is_add:
+                    continue
                 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}"
-                )
+                log.info(f"部门联动 - 删除员工额度: employee_id={employee_id}, institution_id={inst_id}")
 
         await auth.db.flush()
     except Exception as e:

+ 19 - 10
backend/app/plugin/module_payment/notification/handlers/employee_handler.py

@@ -111,14 +111,13 @@ 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,
-            )
+        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:
@@ -128,7 +127,17 @@ class EmployeeHandler(BaseHandler[dict]):
         return True
 
     async def _handle_delete(self, data: EmployeeChangeContent, auth: AuthSchema) -> bool:
-        """处理员工删除"""
-        log.info(f"员工删除: employee_id={data.employee_id}")
-        # await self.update_employee(data, auth)
+        """处理员工删除/离职"""
+        log.info(f"员工删除/离职: employee_id={data.employee_id}")
+        await self.update_employee(data, auth)
+        # 联动:从所有引用该员工的制度中移除
+        if data.employee_id and data.enterprise_id:
+            try:
+                from app.plugin.module_payment.expense.institution.scope_sync import remove_employee_from_institution_scopes
+                await remove_employee_from_institution_scopes(
+                    auth=auth, enterprise_id=data.enterprise_id,
+                    employee_id=data.employee_id,
+                )
+            except Exception as e:
+                log.warning(f"删除员工联动失败(不影响主流程): {e}")
         return True