From bdd67d44d93fbf3ea3770319f0284e29cfb45a02 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sat, 26 Sep 2026 20:55:15 +0800 Subject: [PATCH] feat: add recharge workflow models and schema statuses Co-authored-by: Cursor --- backend/app/api/bx/workflow.py | 29 +++++++- backend/app/models/bx_models.py | 6 ++ backend/app/models/pl_models.py | 36 ++++++++- backend/app/schemas/bx/workflow.py | 2 + backend/app/schemas/pl/vehicle_recharge.py | 73 +++++++++++++------ backend/app/services/bx/workflow.py | 19 +++++ .../test_pl_vehicle_recharge_workflow.py | 8 ++ 7 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 backend/tests/test_pl_vehicle_recharge_workflow.py diff --git a/backend/app/api/bx/workflow.py b/backend/app/api/bx/workflow.py index 86962ed89..3f29104bc 100644 --- a/backend/app/api/bx/workflow.py +++ b/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="工作流不存在") diff --git a/backend/app/models/bx_models.py b/backend/app/models/bx_models.py index 0e7a720c8..5b723deb8 100644 --- a/backend/app/models/bx_models.py +++ b/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="备注") diff --git a/backend/app/models/pl_models.py b/backend/app/models/pl_models.py index 3885c5ad0..dc58fa211 100644 --- a/backend/app/models/pl_models.py +++ b/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="备注") diff --git a/backend/app/schemas/bx/workflow.py b/backend/app/schemas/bx/workflow.py index 1eb05be12..fa9c0536f 100644 --- a/backend/app/schemas/bx/workflow.py +++ b/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): diff --git a/backend/app/schemas/pl/vehicle_recharge.py b/backend/app/schemas/pl/vehicle_recharge.py index 2b5f847b5..ccc73a631 100644 --- a/backend/app/schemas/pl/vehicle_recharge.py +++ b/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 diff --git a/backend/app/services/bx/workflow.py b/backend/app/services/bx/workflow.py index 570c247ab..d99925e8c 100644 --- a/backend/app/services/bx/workflow.py +++ b/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) diff --git a/backend/tests/test_pl_vehicle_recharge_workflow.py b/backend/tests/test_pl_vehicle_recharge_workflow.py new file mode 100644 index 000000000..6d9d68362 --- /dev/null +++ b/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