فهرست منبع

feat: modify 制度同步规则和额度关联表

alphah 2 هفته پیش
والد
کامیت
f72e8f5582

+ 4 - 1
backend/app/plugin/module_payment/expense/institution/controller.py

@@ -153,6 +153,7 @@ async def create_institution_controller(
         enterprise_id=enterprise_id,
         scope_data=scope_data,
         issuerule_data=issuerule_data,
+        raw_data=data,
     )
     log.info(f"创建费控制度成功: institution_id={result.get('institution_id')}")
     return SuccessResponse(data=result, msg="创建费控制度成功")
@@ -265,7 +266,9 @@ async def modify_institution_controller(
     if data.get("expense_type") in EXPENSE_TYPE_MAP:
         data["expense_type"] = EXPENSE_TYPE_MAP[data["expense_type"]]
     institution_modify_model = AlipayEbppInvoiceInstitutionModifyModel(**data)
-    result = await InstitutionService.modify_institution_service(auth=auth, data=institution_modify_model)
+    result = await InstitutionService.modify_institution_service(
+        auth=auth, data=institution_modify_model, raw_data=data
+    )
     log.info(f"编辑费控制度成功: institution_id={institution_modify_model.institution_id}")
     return SuccessResponse(data=result, msg="编辑费控制度成功")
 

+ 182 - 26
backend/app/plugin/module_payment/expense/institution/service.py

@@ -103,6 +103,7 @@ class InstitutionService:
         enterprise_id: str,
         scope_data: dict | None = None,
         issuerule_data: dict | None = None,
+        raw_data: dict | None = None,
     ) -> dict:
         """
         创建费控制度(完整串联流程)
@@ -111,7 +112,7 @@ class InstitutionService:
         1. institution.create → 获取 institution_id
         2. scope.modify ← 如有适用成员数据(scope_data)
         3. issuerule.create ← 如为"按固定周期发放"(issuerule_data)
-        4. 保存到本地DB
+        4. 保存到本地DB(制度 + 使用规则 + 发放规则)
         """
         # 第1步:创建制度
         institution_result = await cls.create_institution_service(auth=auth, data=institution_model)
@@ -194,10 +195,57 @@ class InstitutionService:
         )
 
         create_data_dict = create_data.model_dump(exclude_unset=True)
-        # issue_rule_id 暂不写入模型(模型无该字段),后续扩展
         crud = InstitutionCRUD(auth)
         await crud.create(create_data_dict)
 
+        # 第5步:保存使用规则到本地
+        if raw_data and raw_data.get("standard_info_list") and hasattr(institution_result, 'standard_id_info_list') and institution_result.standard_id_info_list:
+            from app.plugin.module_payment.expense.rule.crud import RuleCRUD
+            from app.plugin.module_payment.expense.rule.service import RuleService
+            standard_id_map = {}
+            for info in institution_result.standard_id_info_list:
+                if hasattr(info, 'outer_source_id') and hasattr(info, 'standard_id'):
+                    standard_id_map[info.outer_source_id] = info.standard_id
+            for idx, std in enumerate(raw_data["standard_info_list"]):
+                std_data = {
+                    "out_biz_no": std.get("outer_source_id", f"std_{institution_id}_{idx}"),
+                    "institution_id": institution_id,
+                    "rule_id": standard_id_map.get(std.get("outer_source_id", "")),
+                    "standard_name": std.get("standard_name"),
+                    "standard_desc": std.get("standard_desc"),
+                    "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
+                    "enterprise_id": enterprise_id,
+                }
+                try:
+                    from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
+                    from sqlalchemy import insert
+                    stmt = insert(ExpenseRuleModel).values(**std_data)
+                    await auth.db.execute(stmt)
+                    await auth.db.flush()
+                except Exception as e:
+                    log.warning(f"保存使用规则到本地失败: {e}")
+
+        # 第6步:保存发放规则到本地
+        if issuerule_data and issue_rule_id:
+            from app.plugin.module_payment.expense.quota.model import QuotaModel
+            quota_save_data = {
+                "employee_id": "",
+                "institution_id": institution_id,
+                "out_biz_no": issuerule_data.get("outer_source_id", f"issue_{institution_id}"),
+                "quota_id": issue_rule_id,
+                "total_amount": float(issuerule_data.get("issue_amount_value", 0)),
+                "available_amount": float(issuerule_data.get("issue_amount_value", 0)),
+                "status": "QUOTA_ACTIVE",
+                "enterprise_id": enterprise_id,
+            }
+            try:
+                from sqlalchemy import insert
+                stmt = insert(QuotaModel).values(**quota_save_data)
+                await auth.db.execute(stmt)
+                await auth.db.flush()
+            except Exception as e:
+                log.warning(f"保存发放规则到本地失败: {e}")
+
         return {
             "institution_id": institution_id,
             "scope_modified": scope_modified,
@@ -375,11 +423,16 @@ class InstitutionService:
 
     @classmethod
     async def modify_institution_service(
-        cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionModifyModel
+        cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionModifyModel, raw_data: dict | None = None
     ) -> AlipayEbppInvoiceInstitutionModifyResponse:
         """
         编辑费控制度
         调用: alipay.ebpp.invoice.institution.modify
+
+        支付宝成功后同步更新本地DB:
+        - 制度基本信息
+        - 使用规则(standard_info_list → pay_expense_rule)
+        - 额度(issuerule → pay_expense_quota)
         """
         if data.institution_id is None:
             raise CustomException(msg="编辑费控制度失败: 制度ID不能为空")
@@ -399,33 +452,136 @@ class InstitutionService:
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             raise CustomException(msg=f"编辑费控制度失败: {result.msg}")
 
-        # 同步更新本地数据库状态
+        # 同步更新本地数据库
+        institution_id = getattr(data, 'institution_id', None)
+        if not institution_id:
+            return result
+
         try:
             crud = InstitutionCRUD(auth)
-            institution_id = getattr(data, 'institution_id', None)
-            if institution_id:
-                update_data = {}
-                if hasattr(data, 'institution_name') and data.institution_name:
-                    update_data['institution_name'] = data.institution_name
-                if hasattr(data, 'institution_desc') and data.institution_desc:
-                    update_data['institution_desc'] = data.institution_desc
-                if hasattr(data, 'effective') and data.effective is not None:
-                    update_data['effective'] = data.effective
-                    update_data['status'] = (
-                        InstitutionStatusEnum.INSTITUTION_EFFECTIVE.value
-                        if data.effective == "1"
-                        else InstitutionStatusEnum.INSTITUTION_INVALID.value
+            update_data = {}
+            if hasattr(data, 'institution_name') and data.institution_name:
+                update_data['institution_name'] = data.institution_name
+            if hasattr(data, 'institution_desc') and data.institution_desc:
+                update_data['institution_desc'] = data.institution_desc
+            if hasattr(data, 'effective') and data.effective is not None:
+                update_data['effective'] = data.effective
+                update_data['status'] = (
+                    InstitutionStatusEnum.INSTITUTION_EFFECTIVE.value
+                    if data.effective == "1"
+                    else InstitutionStatusEnum.INSTITUTION_INVALID.value
+                )
+            if hasattr(data, 'effective_start_date') and data.effective_start_date:
+                update_data['effective_start_date'] = data.effective_start_date
+            if hasattr(data, 'effective_end_date') and data.effective_end_date:
+                update_data['effective_end_date'] = data.effective_end_date
+
+            if update_data:
+                await crud.update_by_institution_id(institution_id, update_data)
+                log.info(f"已更新本地制度: institution_id={institution_id}")
+
+            # 同步标准规则(modify_standard_detail_info)
+            std_detail = (raw_data or {}).get("modify_standard_detail_info") or {}
+            if std_detail:
+                from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
+                from sqlalchemy import delete as sa_delete
+
+                # 删除规则
+                delete_ids = std_detail.get("delete_standard_id_list", [])
+                if delete_ids:
+                    d_stmt = sa_delete(ExpenseRuleModel).where(
+                        ExpenseRuleModel.rule_id.in_(delete_ids)
+                    )
+                    await auth.db.execute(d_stmt)
+
+                # 新增规则
+                add_list = std_detail.get("add_standard_list", [])
+                from sqlalchemy import insert as sa_insert
+                for std in add_list:
+                    ins_data = {
+                        "out_biz_no": std.get("outer_source_id", f"std_{institution_id}"),
+                        "institution_id": institution_id,
+                        "rule_id": std.get("standard_id"),
+                        "standard_name": std.get("standard_name"),
+                        "standard_desc": std.get("standard_desc"),
+                        "expense_type_sub_category": std.get("expense_type_sub_category", "DEFAULT"),
+                        "enterprise_id": raw_data.get("enterprise_id", ""),
+                    }
+                    stmt = sa_insert(ExpenseRuleModel).values(**ins_data)
+                    await auth.db.execute(stmt)
+
+                # 修改规则
+                modify_list = std_detail.get("modify_standard_list", [])
+                for std in modify_list:
+                    std_id = std.get("standard_id", "").strip('"')
+                    update_std = {}
+                    if std.get("standard_name"):
+                        update_std["standard_name"] = std["standard_name"]
+                    if std.get("standard_desc"):
+                        update_std["standard_desc"] = std["standard_desc"]
+                    if update_std and std_id:
+                        from sqlalchemy import update as sa_update
+                        u_stmt = sa_update(ExpenseRuleModel).where(
+                            ExpenseRuleModel.rule_id == std_id
+                        ).values(**update_std)
+                        await auth.db.execute(u_stmt)
+
+                await auth.db.flush()
+                log.info(f"已同步使用规则: institution_id={institution_id}")
+
+            # 同步发放规则(modify_issue_rule_detail_info)
+            issue_detail = (raw_data or {}).get("modify_issue_rule_detail_info") or {}
+            if issue_detail:
+                from app.plugin.module_payment.expense.quota.model import QuotaModel
+                from sqlalchemy import delete as sa_delete
+
+                # 删除发放规则
+                delete_ids = issue_detail.get("delete_issue_rule_id_list", [])
+                if delete_ids:
+                    d_stmt = sa_delete(QuotaModel).where(
+                        QuotaModel.quota_id.in_(delete_ids)
                     )
-                if hasattr(data, 'effective_start_date') and data.effective_start_date:
-                    update_data['effective_start_date'] = data.effective_start_date
-                if hasattr(data, 'effective_end_date') and data.effective_end_date:
-                    update_data['effective_end_date'] = data.effective_end_date
-
-                if update_data:
-                    await crud.update_by_institution_id(institution_id, update_data)
-                    log.info(f"已更新本地记录: institution_id={institution_id}")
+                    await auth.db.execute(d_stmt)
+
+                # 新增发放规则
+                add_list = issue_detail.get("add_issue_rule_list", [])
+                for rule in add_list:
+                    amount = float(rule.get("issue_amount_value", 0))
+                    ins_data = {
+                        "employee_id": "",
+                        "institution_id": institution_id,
+                        "out_biz_no": rule.get("outer_source_id", f"issue_{institution_id}"),
+                        "quota_id": rule.get("issue_rule_id"),
+                        "total_amount": amount,
+                        "available_amount": amount,
+                        "status": "QUOTA_ACTIVE",
+                        "enterprise_id": raw_data.get("enterprise_id", ""),
+                    }
+                    stmt = sa_insert(QuotaModel).values(**ins_data)
+                    await auth.db.execute(stmt)
+
+                # 修改发放规则
+                modify_rule = issue_detail.get("modify_issue_rule_list") or {}
+                if modify_rule.get("issue_rule_id"):
+                    q_id = modify_rule["issue_rule_id"].strip('"')
+                    update_q = {}
+                    if modify_rule.get("issue_amount_value"):
+                        update_q["total_amount"] = float(modify_rule["issue_amount_value"])
+                        update_q["available_amount"] = float(modify_rule["issue_amount_value"])
+                    if modify_rule.get("issue_rule_name"):
+                        update_q["standard_name"] = modify_rule["issue_rule_name"]
+                    if update_q and q_id:
+                        from sqlalchemy import update as sa_update
+                        u_stmt = sa_update(QuotaModel).where(
+                            QuotaModel.quota_id == q_id
+                        ).values(**update_q)
+                        await auth.db.execute(u_stmt)
+
+                await auth.db.flush()
+                log.info(f"已同步发放规则: institution_id={institution_id}")
+
         except Exception as e:
-            log.warning(f"更新本地记录失败(不影响支付宝侧): {e}")
+            log.warning(f"本地同步失败(不影响支付宝侧): {e}")
 
         return result
 

+ 2 - 0
backend/app/plugin/module_payment/notification/enums.py

@@ -15,6 +15,8 @@ class AlipayNotifyMethodEnum(str, Enum):
     CONSUME_CHANGE = "alipay.commerce.ec.consume.change.notify"
     # 发票订单变更通知
     INVOICE_ORDER_CHANGE = "alipay.ebpp.invoice.ecorder.order.changed"
+    # 费控制度操作通知
+    INSTITUTION_OPERATION = "alipay.commerce.ec.institution.operation.notify"
 
 
 class EmployeeActionEnum(str, Enum):

+ 1 - 0
backend/app/plugin/module_payment/notification/handlers/__init__.py

@@ -4,6 +4,7 @@ from .employee_handler import EmployeeHandler
 from .bill_handler import BillHandler, VoucherHandler
 from .order_handler import OrderHandler
 from .account_handler import AccountHandler
+from .institution_handler import InstitutionHandler
 
 __all__ = [
     "BaseHandler",

+ 69 - 0
backend/app/plugin/module_payment/notification/handlers/institution_handler.py

@@ -0,0 +1,69 @@
+from redis.asyncio import Redis
+
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.core.exceptions import CustomException
+from app.core.logger import log
+
+from .base_handler import BaseHandler
+
+
+class InstitutionHandler(BaseHandler[dict]):
+    """费控制度操作通知处理器"""
+
+    async def handle(
+        self, method: str, content: dict, auth: AuthSchema, redis: Redis
+    ) -> bool:
+        """处理费控制度操作通知"""
+        operation_type = content.get("operation_type")
+        institution_id = content.get("institution_id")
+        enterprise_id = content.get("enterprise_id")
+
+        log.info(
+            f"处理费控制度操作通知: operation_type={operation_type}, "
+            f"institution_id={institution_id}"
+        )
+
+        if operation_type == "DELETE":
+            return await self._handle_delete(institution_id, enterprise_id, auth)
+
+        log.info(f"费控制度操作通知无需处理: {operation_type}")
+        return True
+
+    async def _handle_delete(
+        self, institution_id: str, enterprise_id: str, auth: AuthSchema
+    ) -> bool:
+        """处理制度删除:删除本地关联的规则和额度"""
+        try:
+            from app.plugin.module_payment.expense.rule.model import ExpenseRuleModel
+            from app.plugin.module_payment.expense.quota.model import QuotaModel
+            from app.plugin.module_payment.expense.institution.model import ExpenseInstitutionModel
+            from sqlalchemy import delete, select
+
+            # 删除使用规则
+            rule_stmt = delete(ExpenseRuleModel).where(
+                ExpenseRuleModel.institution_id == institution_id
+            )
+            await auth.db.execute(rule_stmt)
+
+            # 删除额度
+            quota_stmt = delete(QuotaModel).where(
+                QuotaModel.institution_id == institution_id
+            )
+            await auth.db.execute(quota_stmt)
+
+            # 删除制度
+            inst_stmt = delete(ExpenseInstitutionModel).where(
+                ExpenseInstitutionModel.institution_id == institution_id
+            )
+            await auth.db.execute(inst_stmt)
+
+            await auth.db.flush()
+            log.info(
+                f"费控制度删除完成: institution_id={institution_id}, "
+                f"已清理关联规则和额度"
+            )
+            return True
+        except Exception as e:
+            log.error(f"费控制度删除失败: {e}")
+            # webhook 处理方法不抛异常,返回 False 让调用方决定
+            return False

+ 6 - 4
backend/app/plugin/module_payment/notification/service.py

@@ -10,13 +10,14 @@ from app.plugin.module_payment.notification.crud import AlipayNotifyLogCRUD
 from .schemas import AlipayNotifyBase
 from .enums import AlipayNotifyMethodEnum
 from .handlers import (
+    AccountHandler,
     BaseHandler,
-    EnterpriseHandler,
-    EmployeeHandler,
     BillHandler,
-    VoucherHandler,
+    EmployeeHandler,
+    EnterpriseHandler,
+    InstitutionHandler,
     OrderHandler,
-    AccountHandler,
+    VoucherHandler,
 )
 
 
@@ -31,6 +32,7 @@ class NotificationService:
         # AlipayNotifyMethodEnum.INVOICE_ORDER_CHANGE.value: OrderHandler,
         AlipayNotifyMethodEnum.TRANS_AUTHORIZE_NOTIFY.value: AccountHandler,
         AlipayNotifyMethodEnum.FUND_CHANGE_NOTIFY.value: AccountHandler,
+        AlipayNotifyMethodEnum.INSTITUTION_OPERATION.value: InstitutionHandler,
     }