Просмотр исходного кода

feat: add recharge workflow models and schema statuses

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 месяц назад
Родитель
Сommit
bdd67d44d9

+ 26 - 3
backend/app/api/bx/workflow.py

@@ -67,7 +67,16 @@ def create_workflow(
     nodes_data = data.pop("nodes", []) or []
     workflow = WorkflowService.create(db, data, user=user)
     if nodes_data:
-        WorkflowService.save_nodes(db, workflow.id, nodes_data, user=user)
+        try:
+            WorkflowService.save_nodes(db, workflow.id, nodes_data, user=user)
+        except ValueError as exc:
+            db.rollback()
+            code = (
+                status.HTTP_400_BAD_REQUEST
+                if "发放登记" in str(exc)
+                else status.HTTP_404_NOT_FOUND
+            )
+            raise HTTPException(status_code=code, detail=str(exc)) from exc
     db.commit()
     db.refresh(workflow)
     return ApiResponse(data=WorkflowDefOut.model_validate(workflow))
@@ -87,7 +96,16 @@ def update_workflow(
     if not workflow:
         raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在")
     if nodes_data is not None:
-        WorkflowService.save_nodes(db, workflow_id, nodes_data, user=user)
+        try:
+            WorkflowService.save_nodes(db, workflow_id, nodes_data, user=user)
+        except ValueError as exc:
+            db.rollback()
+            code = (
+                status.HTTP_400_BAD_REQUEST
+                if "发放登记" in str(exc)
+                else status.HTTP_404_NOT_FOUND
+            )
+            raise HTTPException(status_code=code, detail=str(exc)) from exc
     db.commit()
     db.refresh(workflow)
     return ApiResponse(data=WorkflowDefOut.model_validate(workflow))
@@ -106,7 +124,12 @@ def save_workflow_nodes(
         WorkflowService.save_nodes(db, workflow_id, nodes_data, user=user)
     except ValueError as exc:
         db.rollback()
-        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
+        code = (
+            status.HTTP_400_BAD_REQUEST
+            if "发放登记" in str(exc)
+            else status.HTTP_404_NOT_FOUND
+        )
+        raise HTTPException(status_code=code, detail=str(exc)) from exc
     workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user)
     if not workflow:
         raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在")

+ 6 - 0
backend/app/models/bx_models.py

@@ -73,6 +73,12 @@ class BxWorkflowNode(Base):
     node_order = Column(Integer, nullable=False, default=0, comment="节点顺序")
     approver_id = Column(String(64), nullable=True, comment="审批人用户ID")
     approver_name = Column(String(64), nullable=True, comment="审批人姓名")
+    action_type = Column(
+        String(20),
+        nullable=False,
+        default="approve",
+        comment="approve-审批 disburse-发放登记",
+    )
     tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
     order_num = Column(Integer, nullable=True, comment="排序号(公共)")
     remark = Column(String(512), nullable=True, comment="备注")

+ 35 - 1
backend/app/models/pl_models.py

@@ -1333,7 +1333,41 @@ class PlVehicleRechargeApply(Base):
     odometer_km = Column(DECIMAL(10, 1), nullable=True, comment="期初公里数")
     attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
     applicant_id = Column(String(64), nullable=False, comment="申请人ID")
-    status = Column(String(20), nullable=False, default="pending", comment="申请状态")
+    status = Column(
+        String(20),
+        nullable=False,
+        default="draft",
+        comment="draft/in_approval/rejected/disbursed/cancelled",
+    )
+    wf_id = Column(String(64), nullable=True, comment="工作流定义ID")
+    current_node = Column(String(64), nullable=True, comment="当前节点ID")
+    approval_round = Column(Integer, nullable=False, default=0, comment="当前审批轮次")
+    tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
+    order_num = Column(Integer, nullable=True, comment="排序号")
+    remark = Column(String(512), nullable=True, comment="备注")
+    create_by = Column(String(64), nullable=True, comment="创建人id")
+    create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
+    create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
+    update_by = Column(String(64), nullable=True, comment="更新人id")
+    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
+    del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
+
+
+class PlVehicleRechargeApproval(Base):
+    """车辆充值申请审批历史 pl_vehicle_recharge_approval"""
+
+    __tablename__ = "pl_vehicle_recharge_approval"
+
+    id = Column(String(64), primary_key=True, comment="主键ID")
+    apply_id = Column(String(64), nullable=False, comment="申请单ID")
+    approval_round = Column(Integer, nullable=False, default=1, comment="审批轮次")
+    node_id = Column(String(64), nullable=True, comment="节点ID快照")
+    node_name = Column(String(128), nullable=True, comment="节点名称快照")
+    approver_id = Column(String(64), nullable=False, comment="审批人用户ID")
+    approver_name = Column(String(64), nullable=False, comment="审批人姓名")
+    approval_status = Column(String(32), nullable=False, comment="approved/rejected/disbursed")
+    approval_opinion = Column(String(1024), nullable=True, comment="审批意见")
+    approval_time = Column(DateTime, nullable=False, comment="审批时间")
     tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
     order_num = Column(Integer, nullable=True, comment="排序号")
     remark = Column(String(512), nullable=True, comment="备注")

+ 2 - 0
backend/app/schemas/bx/workflow.py

@@ -17,6 +17,7 @@ class WorkflowNodeCreate(BaseModel):
     node_order: int
     approver_id: Optional[str] = None
     approver_name: Optional[str] = None
+    action_type: Optional[str] = "approve"
 
     @field_validator("approver_id", mode="before")
     @classmethod
@@ -32,6 +33,7 @@ class WorkflowNodeOut(BaseModel):
     node_order: int
     approver_id: Optional[str] = None
     approver_name: Optional[str] = None
+    action_type: Optional[str] = "approve"
 
 
 class WorkflowDefCreate(BaseModel):

+ 51 - 22
backend/app/schemas/pl/vehicle_recharge.py

@@ -1,11 +1,14 @@
 """车辆 ETC/加油充值 Schema"""
 from datetime import date, datetime
 from decimal import Decimal
-from typing import Optional
+from typing import List, Optional
 
 from pydantic import BaseModel, Field, field_validator
 
 RECHARGE_TYPES = {"etc", "fuel"}
+APPLY_STATUSES = {"draft", "in_approval", "rejected", "disbursed", "cancelled"}
+NODE_ACTION_TYPES = {"approve", "disburse"}
+APPROVAL_STATUSES = {"approved", "rejected", "disbursed"}
 
 
 class VehicleRechargeApplyCreate(BaseModel):
@@ -31,6 +34,42 @@ class VehicleRechargeApplyUpdate(VehicleRechargeApplyCreate):
     pass
 
 
+class VehicleRechargeApprovalRead(BaseModel):
+    id: str
+    apply_id: str = Field(..., alias="applyId")
+    approval_round: int = Field(..., alias="approvalRound")
+    node_id: Optional[str] = Field(default=None, alias="nodeId")
+    node_name: Optional[str] = Field(default=None, alias="nodeName")
+    approver_id: str = Field(..., alias="approverId")
+    approver_name: str = Field(..., alias="approverName")
+    approval_status: str = Field(..., alias="approvalStatus")
+    approval_opinion: Optional[str] = Field(default=None, alias="approvalOpinion")
+    approval_time: datetime = Field(..., alias="approvalTime")
+
+    model_config = {"populate_by_name": True}
+
+
+class VehicleRechargeRecordRead(BaseModel):
+    id: str
+    recharge_order_no: str = Field(..., alias="rechargeOrderNo")
+    recharge_date: date = Field(..., alias="rechargeDate")
+    recharge_type: str = Field(..., alias="rechargeType")
+    card_no: Optional[str] = Field(default=None, alias="cardNo")
+    gas_station: Optional[str] = Field(default=None, alias="gasStation")
+    amount: Decimal
+    attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
+    apply_id: Optional[str] = Field(default=None, alias="applyId")
+    apply_no: Optional[str] = Field(default=None, alias="applyNo")
+    license_plate: Optional[str] = Field(default=None, alias="licensePlate")
+    remark: Optional[str] = None
+    create_by: Optional[str] = Field(default=None, alias="createBy")
+    create_by_name: Optional[str] = Field(default=None, alias="createByName")
+    create_time: Optional[datetime] = Field(default=None, alias="createTime")
+    update_time: Optional[datetime] = Field(default=None, alias="updateTime")
+
+    model_config = {"populate_by_name": True}
+
+
 class VehicleRechargeApplyRead(BaseModel):
     id: str
     apply_no: str = Field(..., alias="applyNo")
@@ -42,6 +81,11 @@ class VehicleRechargeApplyRead(BaseModel):
     applicant_id: str = Field(..., alias="applicantId")
     applicant_name: Optional[str] = Field(default=None, alias="applicantName")
     status: str
+    wf_id: Optional[str] = Field(default=None, alias="wfId")
+    current_node: Optional[str] = Field(default=None, alias="currentNode")
+    approval_round: int = Field(default=0, alias="approvalRound")
+    approvals: List[VehicleRechargeApprovalRead] = Field(default_factory=list)
+    record: Optional[VehicleRechargeRecordRead] = None
     remark: Optional[str] = None
     create_time: Optional[datetime] = Field(default=None, alias="createTime")
     update_time: Optional[datetime] = Field(default=None, alias="updateTime")
@@ -67,6 +111,12 @@ class VehicleRechargeApplyOption(BaseModel):
     model_config = {"populate_by_name": True}
 
 
+class VehicleRechargeOpinionBody(BaseModel):
+    opinion: Optional[str] = None
+
+    model_config = {"populate_by_name": True}
+
+
 class VehicleRechargeRecordCreate(BaseModel):
     recharge_date: date = Field(..., alias="rechargeDate")
     recharge_type: str = Field(..., alias="rechargeType")
@@ -99,27 +149,6 @@ class VehicleRechargeRecordUpdate(VehicleRechargeRecordCreate):
     pass
 
 
-class VehicleRechargeRecordRead(BaseModel):
-    id: str
-    recharge_order_no: str = Field(..., alias="rechargeOrderNo")
-    recharge_date: date = Field(..., alias="rechargeDate")
-    recharge_type: str = Field(..., alias="rechargeType")
-    card_no: Optional[str] = Field(default=None, alias="cardNo")
-    gas_station: Optional[str] = Field(default=None, alias="gasStation")
-    amount: Decimal
-    attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
-    apply_id: Optional[str] = Field(default=None, alias="applyId")
-    apply_no: Optional[str] = Field(default=None, alias="applyNo")
-    license_plate: Optional[str] = Field(default=None, alias="licensePlate")
-    remark: Optional[str] = None
-    create_by: Optional[str] = Field(default=None, alias="createBy")
-    create_by_name: Optional[str] = Field(default=None, alias="createByName")
-    create_time: Optional[datetime] = Field(default=None, alias="createTime")
-    update_time: Optional[datetime] = Field(default=None, alias="updateTime")
-
-    model_config = {"populate_by_name": True}
-
-
 class VehicleRechargeRecordListResponse(BaseModel):
     items: list[VehicleRechargeRecordRead]
     total: int

+ 19 - 0
backend/app/services/bx/workflow.py

@@ -20,6 +20,10 @@ _DEFAULT_WORKFLOWS = {
         "wf_name": "报销单审批流程",
         "remark": "报销申请默认审批流程",
     },
+    "vehicle_recharge": {
+        "wf_name": "ETC/加油充值审批流程",
+        "remark": "充值申请默认审批流程",
+    },
 }
 
 
@@ -162,6 +166,18 @@ class WorkflowService:
         if not workflow:
             raise ValueError("工作流不存在")
 
+        disburse_count = 0
+        for node_data in nodes:
+            action = (
+                node_data.get("action_type")
+                or node_data.get("actionType")
+                or "approve"
+            )
+            if action == "disburse":
+                disburse_count += 1
+        if disburse_count > 1:
+            raise ValueError("发放登记节点至多一个")
+
         db.query(BxWorkflowNode).filter(
             BxWorkflowNode.wf_id == wf_id,
             BxWorkflowNode.tenant_no == tenant_no,
@@ -170,6 +186,7 @@ class WorkflowService:
         created: List[BxWorkflowNode] = []
         for node_data in nodes:
             data = dict(node_data)
+            data.pop("actionType", None)
             data["id"] = generate_uuid()
             data["wf_id"] = wf_id
             data["tenant_no"] = tenant_no
@@ -177,6 +194,8 @@ class WorkflowService:
             data["create_dept_id"] = user.dept_id or ""
             if data.get("approver_id") is not None:
                 data["approver_id"] = str(data["approver_id"]).strip() or None
+            if not data.get("action_type"):
+                data["action_type"] = "approve"
             node = BxWorkflowNode(**data)
             db.add(node)
             created.append(node)

+ 8 - 0
backend/tests/test_pl_vehicle_recharge_workflow.py

@@ -0,0 +1,8 @@
+"""车辆充值审批工作流:状态常量与核心流转单元测试。"""
+from app.schemas.pl.vehicle_recharge import APPLY_STATUSES, NODE_ACTION_TYPES, RECHARGE_TYPES
+
+
+def test_apply_statuses_include_workflow_states():
+    assert {"draft", "in_approval", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
+    assert {"approve", "disburse"} <= NODE_ACTION_TYPES
+    assert {"etc", "fuel"} <= RECHARGE_TYPES