|
|
# 间接支付页面 - 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`
|
|
|
|
|
|
```15:27: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',
|
|
|
];
|
|
|
```
|
|
|
|
|
|
**类型转换**:
|
|
|
```29:37:backend/Modules/Budget/app/Models/TemplateElement.php
|
|
|
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行):
|
|
|
|
|
|
```583:620:czemc-budget-execution-frontend/src/views/settings/TemplateElementSettings.vue
|
|
|
// 分类改变处理
|
|
|
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行):
|
|
|
|
|
|
```152:158:backend/Modules/Budget/app/Http/Controllers/Api/TemplateElementController.php
|
|
|
// 验证:事前流程自动设置类型并验证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类型,自动转换为数组)
|
|
|
|
|
|
**配置结构示例**:
|
|
|
```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行
|
|
|
|
|
|
```1158:1200:czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue
|
|
|
// 如果不存在(新分组),从模版加载元素
|
|
|
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行
|
|
|
|
|
|
```706:736:czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue
|
|
|
// 元素选择改变时的处理
|
|
|
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行
|
|
|
|
|
|
```2239:2274:czemc-budget-execution-frontend/src/views/payment/IndirectPayment.vue
|
|
|
// 加载分类的模板
|
|
|
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`,可以通过以下方式补充:
|
|
|
|
|
|
```javascript
|
|
|
// 为所有字段补充 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行
|
|
|
|
|
|
```296:301:czemc-budget-execution-frontend/src/views/payment/IndirectPayment.vue
|
|
|
<!-- 事前流程实例 -->
|
|
|
<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行):
|
|
|
|
|
|
```269:294:czemc-budget-execution-frontend/src/components/PreApprovalFlowPicker.vue
|
|
|
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`)
|
|
|
- 用于查询对应流程模型的流程实例列表
|
|
|
|
|
|
## 六、完整数据流图
|
|
|
|
|
|
```mermaid
|
|
|
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}`
|
|
|
|
|
|
**返回数据**:
|
|
|
```json
|
|
|
{
|
|
|
"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`
|
|
|
|
|
|
**请求参数**:
|
|
|
```javascript
|
|
|
{
|
|
|
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 流程模型
|
|
|
|