You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cz-hjjc-budget/docs/间接支付-modelId参数来源分析.md

16 KiB

间接支付页面 - modelId 参数来源分析

概述

本文档详细分析间接支付页面第三步中 PreApprovalFlowPicker 组件的 modelId 参数的完整数据流,从数据库存储到前端组件使用的全链路追踪。

一、数据源头

1.1 数据库表结构

表名: budget_template_elements

关键字段:

  • id: 模板元素ID主键
  • name: 元素名称
  • type: 元素类型(如 oa_custom_model
  • model_id: OA流程模型ID(关联 oa_custom_model 表的 id
  • category: 元素分类(如 pre_approval_flow
  • options: 选项配置JSON
  • display_conditions: 显示条件JSON

模型文件: backend/Modules/Budget/app/Models/TemplateElement.php

protected $fillable = [
    'name',
    'type',
    'model_id',
    'category',
    'options',
    'description',
    'sort_order',
    'is_active',
    'display_conditions',
    'created_by',
    'updated_by',
];

类型转换:

protected $casts = [
    'model_id' => 'integer',
    'sort_order' => 'integer',
    'is_active' => 'boolean',
    'options' => 'array',
    'display_conditions' => 'array',
    'created_by' => 'integer',
    'updated_by' => 'integer',
];

1.2 model_id 的含义

model_id 指向 OA 系统中的自定义流程模型(oa_custom_model 表),用于标识该模板元素关联的是哪个事前流程模型。

关联表: oa_custom_model

  • 存储 OA 系统中的自定义流程模型定义
  • 每个模型代表一种事前流程类型(如:合同审批、采购申请等)

二、模板元素创建流程

2.1 创建模板元素时设置 model_id

页面: 非直接支付模版元素设置页面 路径: /settings/template-element-settings 组件: czemc-budget-execution-frontend/src/views/settings/TemplateElementSettings.vue

关键逻辑第583-620行:

// 分类改变处理
const handleCategoryChange = () => {
  // 清除相关字段
  if (formData.category !== 'form_field') {
    formData.type = ''
  }
  if (formData.category !== 'checklist') {
    formData.options = []
  }
  if (formData.category !== 'pre_approval_flow') {
    formData.model_id = null
  }
  if (formData.category !== 'detail_table') {
    formData.fields = []
  }
  // 事前流程自动设置类型
  if (formData.category === 'pre_approval_flow') {
    formData.type = 'oa_custom_model'
    // 如果是事前流程且还没有加载模型列表,则加载
    if (availablePreApprovalFlowModels.value.length === 0) {
      loadPreApprovalFlowModels()
    }
  }
  // 勾选清单自动设置类型
  if (formData.category === 'checklist') {
    formData.type = 'checklist'
  }
  // 会议纪要自动设置类型
  if (formData.category === 'meeting_minutes') {
    formData.type = 'meeting_minutes'
    formData.model_id = null
  }
  // 明细表格自动设置类型
  if (formData.category === 'detail_table') {
    formData.type = 'detail_table'
    formData.model_id = null
  }
}

设置规则:

  1. category === 'pre_approval_flow' 时:
    • 自动设置 type = 'oa_custom_model'
    • 必须选择 model_id(从 OA 流程模型列表中选择)
  2. 其他分类时,model_id 会被清空

2.2 后端验证

控制器: backend/Modules/Budget/app/Http/Controllers/Api/TemplateElementController.php

验证逻辑第152-158行:

// 验证事前流程自动设置类型并验证model_id
if ($request->category === 'pre_approval_flow') {
    $request->merge(['type' => 'oa_custom_model']);
    if (!$request->model_id) {
        return $this->fail([400, '事前流程必须提供关联模型ID']);
    }
}

验证规则:

  • 事前流程类型的元素必须提供 model_id
  • 如果未提供,返回 400 错误

三、模板配置存储

3.1 模板配置结构

表名: budget_planned_expenditure_templates

字段: config (JSON类型自动转换为数组)

配置结构示例:

{
  "group_1": {
    "title": "基本信息",
    "fields": [
      {
        "key": "element_123",
        "label": "关联合同审批",
        "element_type": "oa_custom_model",
        "element_id": 123,
        "model_id": 456,  // ← 这里存储了 model_id
        "required": false,
        "visible": true
      }
    ],
    "group_id": 1,
    "group_code": "GROUP_XXX"
  }
}

3.2 模板配置生成流程

页面: 计划支出模板设置页面 路径: /settings/planned-expenditure-template-settings 组件: czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue

3.2.1 从分组加载元素时设置 model_id

位置: CanvasSettings.vue 第1158-1200行

// 如果不存在(新分组),从模版加载元素
const elementsResponse = await preApprovalTemplateGroupAPI.getElements(group.id)
if (elementsResponse.code === 0 && elementsResponse.data) {
  const elements = elementsResponse.data || []
  
  // 确保availableElements已加载
  if (availableElements.value.length === 0) {
    await loadAvailableElements()
  }
  
  // 将元素转换为字段配置
  const fields = elements.map(element => {
    // 从availableElements中查找对应的元素获取完整信息
    const templateElement = availableElements.value.find(e => e.id === element.element_id)
    
    const field = {
      key: `element_${element.element_id}`,
      label: element.element_name,
      element_type: element.element_type,
      required: false,
      visible: true,
      element_id: element.element_id
    }
    
    // 如果是事前流程类型需要获取model_id
    if (element.element_type === 'oa_custom_model' && templateElement && templateElement.model_id) {
      field.model_id = templateElement.model_id
    }
    
    // 如果是勾选清单类型需要获取options
    if (element.element_type === 'checklist' && templateElement && templateElement.options) {
      field.options = templateElement.options
    }
    
    return field
  })
  
  newConfig[sectionKey] = {
    title: group.name,
    fields: fields,
    group_id: group.id,
    group_code: group.code
  }
}

关键步骤:

  1. 从分组中获取元素列表
  2. availableElements 中查找对应的模板元素
  3. 如果是 oa_custom_model 类型,从模板元素中提取 model_id
  4. model_id 添加到字段配置中

3.2.2 手动添加元素时设置 model_id

位置: CanvasSettings.vue 第706-736行

// 元素选择改变时的处理
const onElementSelectChange = () => {
  const selectedElement = availableElements.value.find(e => e.id === elementForm.value.element_id)
  if (selectedElement) {
    elementForm.value.label = selectedElement.name
    elementForm.value.key = `element_${selectedElement.id}`
    
    // 根据元素的category和type设置element_type
    if (selectedElement.category === 'form_field') {
      elementForm.value.element_type = selectedElement.type || 'text'
      elementForm.value.model_id = null
    } else if (selectedElement.category === 'pre_approval_flow' || 
               (!selectedElement.category && selectedElement.model_id && (selectedElement.type === 'oa_custom_model' || selectedElement.type === 'pre_approval_flow'))) {
      // 事前流程category === 'pre_approval_flow' 或 category为空但有model_id
      elementForm.value.element_type = 'oa_custom_model'
      elementForm.value.model_id = selectedElement.model_id || null
    } else if (selectedElement.category === 'checklist') {
      // 勾选清单
      elementForm.value.element_type = 'checklist'
      elementForm.value.model_id = null
    } else if (selectedElement.category === 'meeting_minutes' || selectedElement.type === 'meeting_minutes') {
      // 会议纪要
      elementForm.value.element_type = 'meeting_minutes'
      elementForm.value.model_id = null
    } else if (selectedElement.category === 'detail_table') {
      // 明细表格
      elementForm.value.element_type = 'detail_table'
      elementForm.value.model_id = null
    }
  }
}

关键逻辑:

  • 当选择事前流程类型的元素时,自动从 selectedElement.model_id 获取并设置

四、模板配置加载流程

4.1 间接支付页面加载模板

位置: IndirectPayment.vue 第2239-2274行

// 加载分类的模板
const loadTemplateForCategory = async (categoryId) => {
  try {
    const response = await plannedExpenditureTemplateAPI.getByCategory(categoryId)
    if (response.code === 0) {
      const templates = response.data || []
      
      if (templates.length > 0) {
        const template = templates[0]
        selectedTemplate.value = template
        let config = template.config || {}
        
        // 按照分组的 sort_order 重新排序配置
        config = await sortConfigByGroupOrder(config)
        templateConfig.value = config
        
        // 补充勾选清单的 options 数据
        await enrichChecklistOptions(templateConfig.value)
        
        // 补充明细表格的字段定义
        await enrichDetailTableFields(templateConfig.value)
        
        // 初始化表单数据(使用排序后的配置)
        initializeFormDataFromTemplate(config)
      } else {
        // 没有模板,清空选择
        selectedTemplate.value = null
        templateConfig.value = {}
        ElMessage.warning('该分类未设置模板,无法继续')
      }
    } else {
      ElMessage.error(response.msg || '加载模板失败')
    }
  } catch (error) {
    ElMessage.error('加载模板失败:' + error.message)
  }
}

数据流:

  1. 调用 API 获取分类的模板配置
  2. template.config 中获取配置JSON格式
  3. 配置中已包含字段的 model_id(如果字段类型为 oa_custom_model
  4. 将配置存储到 templateConfig.value

4.2 补充数据enrichChecklistOptions

位置: IndirectPayment.vue 第2276-2348行

虽然这个函数主要用于补充 checklist 的 options但它也会加载所有模板元素确保字段信息完整。

注意: 当前实现中,model_id 已经在模板配置中,不需要额外补充。但如果配置中缺少 model_id,可以通过以下方式补充:

// 为所有字段补充 model_id如果需要
allFields.forEach(field => {
  const element = elementMap.get(field.element_id)
  if (element && element.model_id && field.element_type === 'oa_custom_model') {
    field.model_id = element.model_id
  }
})

五、组件使用

5.1 字段渲染

位置: IndirectPayment.vue 第296-301行

<!-- 事前流程实例 -->
<PreApprovalFlowPicker
  v-else-if="field.element_type === 'oa_custom_model'"
  v-model="formData[field.key]"
  :model-id="field.model_id"
  :placeholder="`请选择${field.label}`"
/>

数据来源:

  • field.model_id: 来自 templateConfig 中的字段配置
  • templateConfig: 从数据库模板配置加载

5.2 组件接收

组件: PreApprovalFlowPicker.vue

Props定义第269-294行:

const props = defineProps({
  modelValue: {
    type: [Number, String, null],
    default: null
  },
  modelId: {
    type: [Number, String],
    required: true
  },
  placeholder: {
    type: String,
    default: '请选择事前流程实例'
  },
  disabled: {
    type: Boolean,
    default: false
  },
  readonly: {
    type: Boolean,
    default: false
  },
  autoLoad: {
    type: Boolean,
    default: false
  }
})

使用:

  • modelId必填属性required: true
  • 用于查询对应流程模型的流程实例列表

六、完整数据流图

graph TD
    A[OA系统 oa_custom_model表] -->|创建流程模型| B[模板元素设置页面]
    B -->|选择流程模型| C[创建TemplateElement记录]
    C -->|存储model_id| D[budget_template_elements表]
    D -->|包含model_id字段| E[模板配置生成]
    E -->|从TemplateElement获取model_id| F[模板配置JSON]
    F -->|保存到config字段| G[budget_planned_expenditure_templates表]
    G -->|加载模板配置| H[间接支付页面]
    H -->|从config中读取| I[templateConfig.value]
    I -->|字段配置包含model_id| J[PreApprovalFlowPicker组件]
    J -->|使用modelId查询| K[OA流程实例列表API]

七、数据流详细步骤

步骤1: 创建模板元素

  1. 用户在"非直接支付模版元素设置"页面创建元素
  2. 选择分类为"事前流程"category = 'pre_approval_flow'
  3. 从OA流程模型列表中选择一个模型设置 model_id
  4. 保存到 budget_template_elements

步骤2: 配置模板

  1. 用户在"计划支出模板设置"页面配置模板
  2. 从分组中选择事前流程类型的元素
  3. 系统从 TemplateElement 中获取 model_id
  4. model_id 写入模板配置的字段中
  5. 保存模板配置到 budget_planned_expenditure_templates

步骤3: 加载模板

  1. 用户在间接支付页面选择分类
  2. 调用 API 获取该分类的模板配置
  3. template.config 中解析字段配置
  4. 字段配置中包含 model_id(如果类型为 oa_custom_model

步骤4: 渲染组件

  1. 遍历 templateConfig 中的字段
  2. field.element_type === 'oa_custom_model'
  3. 渲染 PreApprovalFlowPicker 组件
  4. 传递 :model-id="field.model_id"

步骤5: 组件使用

  1. PreApprovalFlowPicker 接收 modelId prop
  2. 使用 modelId 调用 API 获取流程实例列表
  3. API: GET /api/budget/planned-expenditures/oa-flow-instances?custom_model_id={modelId}

八、关键API接口

8.1 获取模板配置

前端API: plannedExpenditureTemplateAPI.getByCategory(categoryId)

后端路由: GET /budget/planned-expenditure-templates/by-category/{categoryId}

返回数据:

{
  "code": 0,
  "data": [
    {
      "id": 1,
      "category_id": 32,
      "config": {
        "group_1": {
          "fields": [
            {
              "key": "element_123",
              "label": "关联合同审批",
              "element_type": "oa_custom_model",
              "element_id": 123,
              "model_id": 456  // ← 关键字段
            }
          ]
        }
      }
    }
  ]
}

8.2 获取流程实例列表

前端API: plannedExpenditureAPI.getOaFlowInstances(params)

后端路由: GET /api/budget/planned-expenditures/oa-flow-instances

请求参数:

{
  custom_model_id: modelId,  // ← 使用传入的 modelId
  year: 2025,
  page: 1,
  page_size: 15,
  keyword: ''  // 可选
}

九、数据验证和错误处理

9.1 model_id 缺失的情况

可能原因:

  1. 模板配置生成时未正确提取 model_id
  2. 模板元素被删除或修改
  3. 数据迁移或导入时丢失

处理方式:

  • 如果 model_id 缺失,PreApprovalFlowPicker 组件会无法加载流程列表
  • 应该在前端补充数据时检查并补充 model_id

9.2 model_id 无效的情况

可能原因:

  1. OA 系统中的流程模型被删除
  2. model_id 指向不存在的模型

处理方式:

  • API 调用会返回错误
  • 前端显示错误提示

十、总结

10.1 数据流向

OA系统 (oa_custom_model)
    ↓
模板元素 (budget_template_elements.model_id)
    ↓
模板配置 (budget_planned_expenditure_templates.config)
    ↓
前端模板配置 (templateConfig.value)
    ↓
字段配置 (field.model_id)
    ↓
组件属性 (:model-id="field.model_id")
    ↓
API查询参数 (custom_model_id)

10.2 关键点

  1. 数据源头: budget_template_elements.model_id 存储 OA 流程模型ID
  2. 配置存储: 模板配置 JSON 中保存字段的 model_id
  3. 数据传递: 通过组件 props 传递 modelId
  4. API使用: 使用 modelId 查询对应模型的流程实例列表

10.3 注意事项

  1. model_id 必须在创建模板元素时设置(事前流程类型)
  2. 模板配置生成时会自动从模板元素中提取 model_id
  3. 如果模板配置中缺少 model_id,需要补充数据
  4. model_id 必须指向有效的 OA 流程模型