浏览代码

feat(expense): 额度记录创建+有效期判断+前端范围切换清空

- 新增 QUOTA_PENDING 状态枚举
- 创建制度时按适用范围创建员工额度记录
  - 定额+有效期内: ACTIVE(立即可用)
  - 定额+未生效/已过期: PENDING(待发放)
  - 手工发放: PENDING(待发放)
- 修改制度时 scope 变动同步增删额度记录
- 前端适用范围切换时清空对应选项
alphah 2 周之前
父节点
当前提交
227c3e2ea2

+ 235 - 3
backend/app/plugin/module_payment/expense/institution/service.py

@@ -1,5 +1,6 @@
 import asyncio
 from datetime import datetime
+from decimal import Decimal
 
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.alipay import AlipayClient
@@ -243,9 +244,18 @@ class InstitutionService:
                 except Exception as e:
                     log.warning(f"保存使用规则到本地失败: {e}")
 
-        # 第6步:保存发放规则到本地(不写入pay_expense_quota)
-        # 去除假额度写入:额度由外部消费同步时通过
-        # alipay.ebpp.invoice.expensecomsue.outsource.notify 写入真实数据
+        # 第6步:按适用范围创建员工额度记录
+        if scope_data and scope_data.get("adapter_type") and scope_data.get("adapter_type") != "NONE":
+            try:
+                await cls._create_institution_quotas(
+                    auth=auth,
+                    institution_id=institution_id,
+                    enterprise_id=enterprise_id,
+                    scope_data=scope_data,
+                    raw_data=raw_data,
+                )
+            except Exception as e:
+                log.warning(f"创建员工额度记录失败(不影响支付宝侧): {e}")
 
         return {
             "institution_id": institution_id,
@@ -253,6 +263,216 @@ class InstitutionService:
             "issue_rule_id": issue_rule_id,
         }
 
+    @classmethod
+    async def _create_institution_quotas(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        enterprise_id: str,
+        scope_data: dict,
+        raw_data: dict | None = None,
+    ):
+        """按适用范围创建员工额度记录
+
+        规则:
+        - 定额发放 + 制度在有效期内:total=定额值, available=定额值, ACTIVE
+        - 定额发放 + 制度未生效/已过期:total=0, available=0, PENDING
+        - 手工发放:total=0, available=0, PENDING
+        """
+        from app.plugin.module_payment.expense.quota.model import QuotaModel
+        from app.plugin.module_payment.expense.quota.enums import QuotaStatusEnum
+        from sqlalchemy import insert, select
+
+        grant_mode = (raw_data or {}).get("grant_mode", "manual")
+        amount_val = float((raw_data or {}).get("amount", 0) or 0)
+        tenant_id = auth.user.tenant_id if auth.user else 1
+
+        # 判断制度是否在有效期内
+        now = datetime.now()
+        is_in_period = True
+        try:
+            start_str = (raw_data or {}).get("effective_start_date")
+            end_str = (raw_data or {}).get("effective_end_date")
+            if start_str:
+                start_dt = datetime.fromisoformat(str(start_str).replace('Z', '').replace('T', ' ')[:19])
+                if now < start_dt:
+                    is_in_period = False
+            if end_str and is_in_period:
+                end_dt = datetime.fromisoformat(str(end_str).replace('Z', '').replace('T', ' ')[:19])
+                if now > end_dt:
+                    is_in_period = False
+        except Exception:
+            is_in_period = True
+
+        # 定额+有效期内 → ACTIVE,否则 → PENDING
+        is_active = (grant_mode == "period") and amount_val > 0 and is_in_period
+
+        # 收集员工ID列表
+        employee_ids: list[str] = []
+        adapter_type = scope_data.get("adapter_type", "")
+        add_ids = scope_data.get("add_owner_id_list") or []
+
+        if adapter_type == "EMPLOYEE_SELECT":
+            # 按员工选择 → 直接使用传入的员工ID
+            employee_ids = [str(i) for i in add_ids if i]
+        elif adapter_type == "EMPLOYEE_DEPARTMENT":
+            # 按部门 → 查该部门下的所有员工
+            for dept_id in add_ids:
+                dept_id_str = str(dept_id)
+                from app.plugin.module_payment.employee.model import EmployeeModel
+                emp_stmt = select(EmployeeModel).where(
+                    EmployeeModel.enterprise_id == enterprise_id,
+                    EmployeeModel.status == "EMPLOYEE_ACTIVATED",
+                )
+                emp_result = await auth.db.execute(emp_stmt)
+                for emp in emp_result.scalars().all():
+                    if emp.department_ids and dept_id_str in emp.department_ids:
+                        employee_ids.append(emp.employee_id)
+        elif adapter_type == "EMPLOYEE_ALL":
+            # 全部员工
+            from app.plugin.module_payment.employee.model import EmployeeModel
+            emp_stmt = select(EmployeeModel).where(
+                EmployeeModel.enterprise_id == enterprise_id,
+                EmployeeModel.status == "EMPLOYEE_ACTIVATED",
+            )
+            emp_result = await auth.db.execute(emp_stmt)
+            employee_ids = [emp.employee_id for emp in emp_result.scalars().all() if emp.employee_id]
+
+        # 去重
+        employee_ids = list(set(employee_ids))
+
+        if not employee_ids:
+            log.info(f"无员工需要创建额度记录: institution_id={institution_id}")
+            return
+
+        now = datetime.now()
+
+        if is_active:
+            # 定额+有效期内:直接赋值
+            total = Decimal(str(amount_val))
+            available = total
+            status = QuotaStatusEnum.QUOTA_ACTIVE.value
+        else:
+            # 手工发放 或 定额但未到有效期:待发放
+            total = Decimal("0")
+            available = Decimal("0")
+            status = QuotaStatusEnum.QUOTA_PENDING.value
+
+        for emp_id in employee_ids:
+            # 检查是否已有记录,避免重复
+            check = select(QuotaModel).where(
+                QuotaModel.employee_id == emp_id,
+                QuotaModel.institution_id == institution_id,
+            )
+            existing = await auth.db.execute(check)
+            if existing.scalar_one_or_none():
+                continue
+
+            stmt = insert(QuotaModel).values(
+                employee_id=emp_id,
+                institution_id=institution_id,
+                out_biz_no=f"inst_{institution_id}_{emp_id}",
+                total_amount=total,
+                available_amount=available,
+                status=status,
+                enterprise_id=enterprise_id,
+                tenant_id=tenant_id,
+            )
+            await auth.db.execute(stmt)
+
+        await auth.db.flush()
+        log.info(
+            f"创建员工额度记录完成: institution_id={institution_id}, "
+            f"count={len(employee_ids)}, mode={'period' if (grant_mode == 'period') and amount_val > 0 else 'manual'}, "
+            f"status={status}, amount={float(total)}, in_period={is_in_period}"
+        )
+
+    @classmethod
+    async def _sync_modify_quotas_by_scope(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        enterprise_id: str,
+        scope_info: dict,
+        raw_data: dict | None = None,
+    ):
+        """修改制度时同步员工额度记录
+
+        新增员工:按发放模式+有效期创建额度记录
+        删除员工:删除对应额度记录
+        """
+        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
+
+        grant_mode = (raw_data or {}).get("grant_mode", "manual")
+        amount_val = float((raw_data or {}).get("amount", 0) or 0)
+        tenant_id = auth.user.tenant_id if auth.user else 1
+
+        # 判断制度是否在有效期内
+        now = datetime.now()
+        is_in_period = True
+        try:
+            start_str = (raw_data or {}).get("effective_start_date")
+            end_str = (raw_data or {}).get("effective_end_date")
+            if start_str:
+                start_dt = datetime.fromisoformat(str(start_str).replace('Z', '').replace('T', ' ')[:19])
+                if now < start_dt:
+                    is_in_period = False
+            if end_str and is_in_period:
+                end_dt = datetime.fromisoformat(str(end_str).replace('Z', '').replace('T', ' ')[:19])
+                if now > end_dt:
+                    is_in_period = False
+        except Exception:
+            is_in_period = True
+
+        is_active = (grant_mode == "period") and amount_val > 0 and is_in_period
+
+        # 删除被移除的员工额度
+        delete_ids = scope_info.get("delete_owner_id_list") or []
+        if delete_ids:
+            del_stmt = sa_delete(QuotaModel).where(
+                QuotaModel.institution_id == institution_id,
+                QuotaModel.employee_id.in_(delete_ids),
+            )
+            await auth.db.execute(del_stmt)
+            log.info(f"删除已移除员工额度: count={len(delete_ids)}")
+
+        # 新增员工的额度
+        add_ids = scope_info.get("add_owner_id_list") or []
+        if add_ids:
+            total = Decimal(str(amount_val)) if is_active else Decimal("0")
+            available = total
+            status = QuotaStatusEnum.QUOTA_ACTIVE.value if is_active else QuotaStatusEnum.QUOTA_PENDING.value
+
+            created = 0
+            for emp_id in add_ids:
+                emp_id_str = str(emp_id)
+                check = select(QuotaModel).where(
+                    QuotaModel.employee_id == emp_id_str,
+                    QuotaModel.institution_id == institution_id,
+                )
+                existing = await auth.db.execute(check)
+                if existing.scalar_one_or_none():
+                    continue
+
+                stmt = insert(QuotaModel).values(
+                    employee_id=emp_id_str,
+                    institution_id=institution_id,
+                    out_biz_no=f"inst_{institution_id}_{emp_id_str}",
+                    total_amount=total,
+                    available_amount=available,
+                    status=status,
+                    enterprise_id=enterprise_id,
+                    tenant_id=tenant_id,
+                )
+                await auth.db.execute(stmt)
+                created += 1
+
+            if created:
+                await auth.db.flush()
+                log.info(f"新增员工额度: count={created}, is_active={is_active}, status={status}")
+
     @classmethod
     async def pageinfo_query_service(
         cls,
@@ -553,6 +773,18 @@ class InstitutionService:
             except Exception as e:
                 log.warning(f"适用范围同步失败(不影响基础修改,本地DB将更新为最新值): {e}")
 
+            # scope 变动后同步员工额度记录
+            try:
+                await cls._sync_modify_quotas_by_scope(
+                    auth=auth,
+                    institution_id=institution_id,
+                    enterprise_id=enterprise_id,
+                    scope_info=scope_info,
+                    raw_data=raw_data,
+                )
+            except Exception as e:
+                log.warning(f"同步员工额度记录失败(不影响主体操作): {e}")
+
         applicable_scope = raw_data.get("applicable_scope", "")
 
         # 第2步:同步更新本地数据库(scope 已在 Alipay modify 请求中通过 modify_scope_info 处理)

+ 1 - 0
backend/app/plugin/module_payment/expense/quota/enums.py

@@ -6,3 +6,4 @@ class QuotaStatusEnum(str, enum.Enum):
     QUOTA_FROZEN = "QUOTA_FROZEN"
     QUOTA_EXHAUSTED = "QUOTA_EXHAUSTED"
     QUOTA_EXPIRED = "QUOTA_EXPIRED"
+    QUOTA_PENDING = "QUOTA_PENDING"

+ 13 - 0
frontend/src/views/module_payment/institution/components/InstitutionForm.vue

@@ -366,6 +366,19 @@ watch(
   (val) => { if (val) loadDepartments(); }
 );
 
+// 适用范围切换时清空不相关选项
+watch(
+  () => formData.applicable_scope,
+  (val) => {
+    if (val !== "employee") {
+      formData.employee_ids = [];
+    }
+    if (val !== "department") {
+      formData.department_id = undefined;
+    }
+  }
+);
+
 const rules = reactive({
   enterprise_id: [{ required: true, message: "请输入企业ID", trigger: "blur" }],
   name: [{ required: true, message: "请输入制度名称", trigger: "blur" }],