样品管理中提交工单

main
risingLee 2026-01-07 01:10:51 +08:00
parent 5a09430e31
commit 4b21dcd1ce
8 changed files with 1209 additions and 2 deletions

View File

@ -0,0 +1,373 @@
# Design Document
## Overview
本设计文档描述了从样品库直接生成测试工单的功能实现。该功能将简化原有的"订单→产品→工单"流程,改为"样品→工单"的直接流程。
核心变化:
- 前端:在样品管理页面添加"生成工单"功能
- 后端创建新的API接口直接从样品信息生成工单
- 数据关联工单的test_eut_id字段将直接关联warehouse_sample表的sample_id
## Architecture
### 系统架构
```
前端层 (Vue.js)
└── 样品管理页面 (warehouse/sample/index.vue)
├── 样品列表展示
├── 样品选择(多选)
└── 生成工单对话框
├── 测试流程选择
├── 工单名称输入
└── 备注输入
API层 (FastAPI)
└── 样品工单控制器 (warehouse_sample_controller.py)
└── POST /warehouse/sample/generate_work_orders
├── 接收样品ID列表
├── 接收测试流程ID
└── 返回生成结果
服务层 (Service)
└── 样品工单服务 (warehouse_sample_service.py)
└── generate_work_orders_from_samples()
├── 验证样品状态
├── 获取测试流程配置
├── 为每个样品创建工单
└── 更新样品状态
数据层 (DAO)
└── 工单数据访问 (test_work_order_dao.py)
└── 批量创建工单记录
```
### 数据流
```
用户选择样品 → 选择测试流程 → 提交请求
验证样品状态 → 获取流程配置 → 查询测试单元
创建工单记录 → 更新样品状态 → 返回结果
```
## Components and Interfaces
### 前端组件
#### 1. 样品管理页面增强 (warehouse/sample/index.vue)
**新增数据字段:**
```javascript
{
// 工单生成对话框
workOrderDialogVisible: false,
// 工单表单
workOrderForm: {
sampleIds: [], // 选中的样品ID列表
testFlowId: null, // 测试流程ID
workOrderName: '', // 工单名称
memo: '' // 备注
},
// 测试流程选项
testFlowOptions: [],
// 测试流程标签
testFlowTags: []
}
```
**新增方法:**
```javascript
// 打开生成工单对话框
handleGenerateWorkOrder()
// 加载测试流程选项
loadTestFlowOptions()
// 获取测试流程标签
fetchTestFlowTags(flowId)
// 提交工单生成
submitWorkOrderGeneration()
```
### 后端接口
#### 1. 样品工单生成API
**端点:** `POST /warehouse/sample/generate_work_orders`
**请求体:**
```json
{
"sampleIds": [1, 2, 3],
"testFlowId": 1,
"workOrderName": "批次测试",
"memo": "紧急测试"
}
```
**响应:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"successCount": 3,
"failedCount": 0,
"workOrderIds": [101, 102, 103],
"failedSamples": []
}
}
```
#### 2. 测试流程查询API复用现有
**端点:** `GET /system/test_flow/list`
**响应:**
```json
{
"code": 200,
"data": [
{
"id": 1,
"name": "标准测试流程",
"tags": [...]
}
]
}
```
## Data Models
### 样品工单生成请求模型
```python
class GenerateWorkOrderFromSampleModel(BaseModel):
"""从样品生成工单的请求模型"""
model_config = ConfigDict(alias_generator=to_camel)
sample_ids: List[int] = Field(description='样品ID列表')
test_flow_id: int = Field(description='测试流程ID')
work_order_name: Optional[str] = Field(default=None, description='工单名称')
memo: Optional[str] = Field(default=None, description='备注')
```
### 工单生成响应模型
```python
class WorkOrderGenerationResponseModel(BaseModel):
"""工单生成响应模型"""
model_config = ConfigDict(alias_generator=to_camel)
success_count: int = Field(description='成功数量')
failed_count: int = Field(description='失败数量')
work_order_ids: List[int] = Field(description='生成的工单ID列表')
failed_samples: List[dict] = Field(description='失败的样品信息')
```
### 数据库表关系
```
warehouse_sample (样品表)
├── sample_id (主键)
├── receipt_id (入库单ID)
├── sample_sn (样品SN号)
├── sample_model (样品型号)
├── hardware_version (硬件版本)
└── status (状态: 0-待测试, 1-测试中, 2-已完成, 3-已退回)
test_work_order (测试工单表)
├── id (主键)
├── test_eut_id (产品ID) → 关联 warehouse_sample.sample_id
├── test_category_id (测试类别ID)
├── test_item_id (测试单元ID)
├── tester_id (测试人ID)
├── reviewer_id (审核人ID)
├── test_status (测试状态)
└── test_step (测试步骤)
test_flow (测试流程表)
├── id (主键)
└── name (流程名称)
test_flow_tags (流程标签关联表)
├── test_flow_id (流程ID)
└── test_category_id (测试类别ID)
test_item (测试单元表)
├── id (主键)
├── test_category_id (测试类别ID)
└── eut_type_id (产品类型ID)
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: 工单创建完整性
*For any* 有效的样品ID列表和测试流程ID系统应为每个样品创建与测试流程中测试类别数量相等的工单数
**Validates: Requirements 2.3**
### Property 2: 样品状态一致性
*For any* 成功生成工单的样品,其状态应从"待测试"(0)更新为"测试中"(1)
**Validates: Requirements 4.4**
### Property 3: 工单数据关联正确性
*For any* 生成的工单其test_eut_id字段应等于对应样品的sample_id
**Validates: Requirements 3.2**
### Property 4: 测试类别映射正确性
*For any* 测试流程生成的工单的test_category_id应属于该流程的test_flow_tags集合
**Validates: Requirements 2.2**
### Property 5: 批量操作原子性
*For any* 样品列表中的单个样品工单创建失败,不应影响其他样品的工单创建
**Validates: Requirements 2.4**
### Property 6: 工单命名规则一致性
*For any* 未指定工单名称的请求,生成的工单名称应遵循"样品SN + 测试类别"的格式
**Validates: Requirements 5.5**
## Error Handling
### 错误类型
1. **样品不存在错误**
- 错误码404
- 消息样品ID不存在
- 处理:跳过该样品,继续处理其他样品
2. **样品状态错误**
- 错误码400
- 消息:样品状态不允许生成工单
- 处理:记录失败原因,继续处理其他样品
3. **测试流程不存在错误**
- 错误码404
- 消息:测试流程不存在
- 处理:终止整个操作,返回错误
4. **测试单元未配置错误**
- 错误码400
- 消息:该样品型号未配置测试单元
- 处理:跳过该样品,记录警告
5. **数据库操作错误**
- 错误码500
- 消息:工单创建失败
- 处理:回滚事务,返回错误详情
### 错误响应格式
```json
{
"code": 400,
"msg": "部分样品工单生成失败",
"data": {
"successCount": 2,
"failedCount": 1,
"workOrderIds": [101, 102],
"failedSamples": [
{
"sampleId": 3,
"sampleSn": "SN003",
"reason": "样品状态不允许生成工单"
}
]
}
}
```
## Testing Strategy
### Unit Tests
1. **样品状态验证测试**
- 测试待测试状态样品可以生成工单
- 测试其他状态样品被正确拒绝
2. **测试流程配置查询测试**
- 测试正确获取流程的测试类别
- 测试流程不存在时的错误处理
3. **工单数据构造测试**
- 测试工单字段正确映射样品信息
- 测试工单命名规则
### Property-Based Tests
使用Python的`hypothesis`库进行属性测试:
1. **Property 1测试工单创建完整性**
```python
@given(
sample_ids=st.lists(st.integers(min_value=1), min_size=1, max_size=10),
test_flow_id=st.integers(min_value=1)
)
async def test_work_order_creation_completeness(sample_ids, test_flow_id):
"""测试为每个样品创建正确数量的工单"""
# 生成工单
result = await generate_work_orders_from_samples(sample_ids, test_flow_id)
# 获取流程的测试类别数量
category_count = await get_test_category_count(test_flow_id)
# 验证:成功的样品数 * 测试类别数 = 工单数
assert len(result.work_order_ids) == result.success_count * category_count
```
2. **Property 2测试样品状态一致性**
```python
@given(sample_ids=st.lists(st.integers(min_value=1), min_size=1))
async def test_sample_status_consistency(sample_ids):
"""测试成功生成工单后样品状态更新"""
# 生成工单
result = await generate_work_orders_from_samples(sample_ids, test_flow_id=1)
# 验证:所有成功的样品状态都是"测试中"
for sample_id in sample_ids[:result.success_count]:
sample = await get_sample_by_id(sample_id)
assert sample.status == '1'
```
3. **Property 3测试工单数据关联正确性**
```python
@given(sample_id=st.integers(min_value=1))
async def test_work_order_sample_association(sample_id):
"""测试工单正确关联样品"""
# 生成工单
result = await generate_work_orders_from_samples([sample_id], test_flow_id=1)
# 验证所有工单的test_eut_id都等于sample_id
for work_order_id in result.work_order_ids:
work_order = await get_work_order_by_id(work_order_id)
assert work_order.test_eut_id == sample_id
```
### Integration Tests
1. **端到端工单生成测试**
- 创建测试样品
- 调用工单生成API
- 验证工单创建成功
- 验证样品状态更新
2. **批量操作测试**
- 创建多个测试样品
- 批量生成工单
- 验证部分失败场景
3. **前端集成测试**
- 测试对话框打开和关闭
- 测试表单验证
- 测试API调用和响应处理

View File

@ -0,0 +1,74 @@
# Requirements Document
## Introduction
本需求文档描述了从样品库直接生成测试工单的功能。原有系统通过订单管理来生成工单现在需求变更为直接从样品库warehouse_sample生成工单简化流程。
## Glossary
- **Sample (样品)**: 入库的测试样品包含型号、SN号、硬件版本等信息
- **Work Order (工单)**: 测试工单,用于跟踪样品的测试流程
- **Test Category (测试类别)**: 测试的分类,如功能测试、性能测试等
- **Test Item (测试单元)**: 具体的测试项目
- **Test Flow (测试流程)**: 包含多个测试类别的完整测试流程
- **Receipt (入库单)**: 样品的入库记录
## Requirements
### Requirement 1
**User Story:** 作为测试人员,我希望能够从样品管理页面直接选择样品生成工单,以便简化工单创建流程。
#### Acceptance Criteria
1. WHEN 用户在样品管理页面选择一个或多个样品 THEN 系统应显示"生成工单"按钮
2. WHEN 用户点击"生成工单"按钮 THEN 系统应打开工单生成对话框
3. WHEN 用户在对话框中选择测试流程 THEN 系统应显示该流程包含的测试类别标签
4. WHEN 用户提交工单生成请求 THEN 系统应为每个选中的样品创建对应的测试工单
5. WHEN 工单生成成功 THEN 系统应显示成功消息并刷新样品列表
### Requirement 2
**User Story:** 作为测试人员,我希望能够为批量样品选择测试流程,以便一次性创建多个工单。
#### Acceptance Criteria
1. WHEN 用户选择多个样品生成工单 THEN 系统应允许为所有样品选择相同的测试流程
2. WHEN 用户选择测试流程 THEN 系统应从测试流程配置中获取包含的测试类别
3. WHEN 系统创建工单 THEN 每个样品应根据测试流程中的测试类别生成对应数量的工单
4. WHEN 某个样品的工单创建失败 THEN 系统应继续创建其他样品的工单并记录失败信息
### Requirement 3
**User Story:** 作为系统管理员,我希望工单能够正确关联样品信息,以便追溯测试对象。
#### Acceptance Criteria
1. WHEN 系统创建工单 THEN 工单应包含样品的SN号作为工单名称的一部分
2. WHEN 系统创建工单 THEN 工单应关联样品IDtest_eut_id字段
3. WHEN 系统创建工单 THEN 工单应关联测试类别IDtest_category_id字段
4. WHEN 系统创建工单 THEN 工单应关联测试单元IDtest_item_id字段
5. WHEN 系统创建工单 THEN 工单应记录创建人信息
### Requirement 4
**User Story:** 作为测试人员,我希望只能为特定状态的样品生成工单,以避免重复创建。
#### Acceptance Criteria
1. WHEN 样品状态为"待测试"(status='0') THEN 系统应允许为该样品生成工单
2. WHEN 样品已有关联的工单 THEN 系统应提示用户该样品已生成工单
3. WHEN 用户确认为已有工单的样品重新生成 THEN 系统应允许创建新工单
4. WHEN 工单生成成功 THEN 系统可选择性地更新样品状态为"测试中"(status='1')
### Requirement 5
**User Story:** 作为测试人员,我希望能够在工单生成对话框中输入工单备注,以便记录特殊说明。
#### Acceptance Criteria
1. WHEN 用户打开工单生成对话框 THEN 系统应提供工单名称输入框
2. WHEN 用户打开工单生成对话框 THEN 系统应提供备注输入框
3. WHEN 用户输入工单名称 THEN 该名称应应用于所有生成的工单
4. WHEN 用户输入备注 THEN 该备注应应用于所有生成的工单
5. WHEN 用户未输入工单名称 THEN 系统应使用默认命名规则样品SN + 测试类别)

View File

@ -0,0 +1,195 @@
# Implementation Plan
- [x] 1. 创建后端数据模型和VO
- 创建GenerateWorkOrderFromSampleModel请求模型
- 创建WorkOrderGenerationResponseModel响应模型
- 确保模型支持camelCase和snake_case转换
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
- [ ] 2. 实现后端服务层逻辑
- [x] 2.1 在warehouse_sample_service.py中实现generate_work_orders_from_samples方法
- 验证样品ID列表有效性
- 验证测试流程ID有效性
- 查询样品信息并验证状态
- 获取测试流程的测试类别列表
- _Requirements: 1.4, 2.1, 4.1, 4.2_
- [x] 2.2 实现工单创建逻辑
- 为每个样品和测试类别组合创建工单
- 查询对应的测试单元(test_item)
- 查询测试任务(test_job)获取测试人员信息
- 构造工单数据并保存
- _Requirements: 2.3, 3.2, 3.3, 3.4, 3.5_
- [x] 2.3 实现样品状态更新逻辑
- 工单创建成功后更新样品状态为"测试中"
- 处理批量更新场景
- _Requirements: 4.4_
- [x] 2.4 实现错误处理和结果汇总
- 捕获单个样品的创建失败,不影响其他样品
- 记录失败原因
- 返回成功和失败的统计信息
- _Requirements: 2.4_
- [ ] 2.5 编写服务层单元测试
- 测试样品状态验证逻辑
- 测试测试流程配置查询
- 测试工单数据构造
- 测试错误处理逻辑
- _Requirements: 1.4, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.4_
- [ ] 3. 实现后端API控制器
- [x] 3.1 在warehouse_sample_controller.py中添加generate_work_orders端点
- 定义POST /warehouse/sample/generate_work_orders路由
- 接收GenerateWorkOrderFromSampleModel请求
- 调用服务层方法
- 返回WorkOrderGenerationResponseModel响应
- 添加权限验证
- _Requirements: 1.2, 1.4_
- [x] 3.2 添加请求验证
- 验证sample_ids不为空
- 验证test_flow_id有效
- 添加适当的错误响应
- _Requirements: 1.4_
- [ ] 3.3 编写控制器单元测试
- 测试正常请求处理
- 测试参数验证
- 测试权限验证
- _Requirements: 1.2, 1.4_
- [ ] 4. 实现前端样品管理页面增强
- [x] 4.1 在warehouse/sample/index.vue中添加"生成工单"按钮
- 在操作按钮区域添加"生成工单"按钮
- 按钮仅在选中样品时启用
- 添加权限控制
- _Requirements: 1.1_
- [x] 4.2 添加工单生成对话框
- 创建el-dialog组件
- 添加测试流程选择下拉框
- 添加工单名称输入框
- 添加备注输入框
- 显示选中的样品数量
- _Requirements: 1.2, 5.1, 5.2_
- [x] 4.3 实现测试流程选择功能
- 加载测试流程选项列表
- 监听测试流程变化
- 获取并显示测试流程标签
- 使用彩色标签展示测试类别
- _Requirements: 1.3, 2.2_
- [x] 4.4 实现工单生成提交逻辑
- 收集表单数据
- 调用后端API
- 处理成功响应(显示成功消息,刷新列表)
- 处理失败响应(显示错误详情)
- 关闭对话框
- _Requirements: 1.4, 1.5_
- [x] 4.5 添加前端数据字段和方法
- 添加workOrderDialogVisible状态
- 添加workOrderForm表单对象
- 添加testFlowOptions选项列表
- 添加testFlowTags标签列表
- 实现handleGenerateWorkOrder方法
- 实现loadTestFlowOptions方法
- 实现fetchTestFlowTags方法
- 实现submitWorkOrderGeneration方法
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [ ] 4.6 编写前端组件测试
- 测试对话框打开和关闭
- 测试表单验证
- 测试API调用
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [ ] 5. 创建API接口文件
- [x] 5.1 在ruoyi-fastapi-frontend/src/api/warehouse/sample.js中添加generateWorkOrders方法
- 定义POST请求方法
- 配置请求URL和参数
- 导出方法供组件使用
- _Requirements: 1.4_
- [ ] 6. 测试和验证
- [ ] 6.1 端到端测试
- 创建测试样品数据
- 测试单个样品生成工单
- 测试批量样品生成工单
- 验证工单数据正确性
- 验证样品状态更新
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5, 4.1, 4.2, 4.4_
- [ ] 6.2 属性测试
- **Property 1: 工单创建完整性**
- **Validates: Requirements 2.3**
- [ ] 6.3 属性测试
- **Property 2: 样品状态一致性**
- **Validates: Requirements 4.4**
- [ ] 6.4 属性测试
- **Property 3: 工单数据关联正确性**
- **Validates: Requirements 3.2**
- [ ] 6.5 属性测试
- **Property 4: 测试类别映射正确性**
- **Validates: Requirements 2.2**
- [ ] 6.6 属性测试
- **Property 5: 批量操作原子性**
- **Validates: Requirements 2.4**
- [ ] 6.7 属性测试
- **Property 6: 工单命名规则一致性**
- **Validates: Requirements 5.5**
- [ ] 7. 错误场景测试
- [ ] 7.1 测试样品不存在场景
- 提供不存在的样品ID
- 验证错误处理
- _Requirements: 2.4_
- [ ] 7.2 测试样品状态不符合场景
- 使用非"待测试"状态的样品
- 验证被正确拒绝
- _Requirements: 4.1, 4.2_
- [ ] 7.3 测试测试流程不存在场景
- 提供不存在的测试流程ID
- 验证错误响应
- _Requirements: 2.1_
- [ ] 7.4 测试部分失败场景
- 混合有效和无效样品
- 验证部分成功的处理
- 验证失败信息记录
- _Requirements: 2.4_
- [ ] 8. 最终检查点
- 确保所有测试通过
- 验证前后端集成正常
- 检查用户体验流畅性
- 如有问题,询问用户

View File

@ -8,7 +8,8 @@ from module_admin.service.login_service import LoginService
from module_admin.service.warehouse_sample_service import WarehouseSampleService
from module_admin.entity.vo.warehouse_sample_vo import (
WarehouseSamplePageQueryModel, AddWarehouseSampleModel,
EditWarehouseSampleModel, DeleteWarehouseSampleModel
EditWarehouseSampleModel, DeleteWarehouseSampleModel,
GenerateWorkOrderFromSampleModel
)
from module_admin.entity.vo.user_vo import CurrentUserModel
from utils.log_util import logger
@ -100,3 +101,37 @@ async def delete_warehouse_sample(
return ResponseUtil.success(msg=result.message)
@warehouseSampleController.post('/generate_work_orders', dependencies=[Depends(CheckUserInterfaceAuth('warehouse:sample:add'))])
@Log(title='样品生成工单', business_type=BusinessType.INSERT)
async def generate_work_orders_from_samples(
request: Request,
generate_request: GenerateWorkOrderFromSampleModel,
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user)
):
"""
从样品生成工单
"""
try:
result = await WarehouseSampleService.generate_work_orders_from_samples(
query_db,
generate_request,
current_user.user.user_id
)
# 根据结果返回不同的消息
if result.failed_count == 0:
message = f'成功为 {result.success_count} 个样品生成工单'
elif result.success_count == 0:
message = f'工单生成失败,{result.failed_count} 个样品处理失败'
else:
message = f'部分成功:{result.success_count} 个样品成功,{result.failed_count} 个样品失败'
logger.info(message)
return ResponseUtil.success(data=result.model_dump(by_alias=True), msg=message)
except Exception as e:
logger.error(f'生成工单失败: {str(e)}')
return ResponseUtil.error(msg=f'生成工单失败: {str(e)}')

View File

@ -1,7 +1,7 @@
from datetime import date, datetime
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from typing import Optional
from typing import Optional, List
class WarehouseSampleModel(BaseModel):
@ -84,3 +84,39 @@ class DeleteWarehouseSampleModel(BaseModel):
class GenerateWorkOrderFromSampleModel(BaseModel):
"""
从样品生成工单的请求模型
"""
model_config = ConfigDict(alias_generator=to_camel)
sample_ids: List[int] = Field(description='样品ID列表')
test_flow_id: int = Field(description='测试流程ID')
work_order_name: Optional[str] = Field(default=None, description='工单名称')
memo: Optional[str] = Field(default=None, description='备注')
force: Optional[bool] = Field(default=False, description='是否强制生成(忽略样品状态检查)')
class FailedSampleInfo(BaseModel):
"""
失败的样品信息
"""
model_config = ConfigDict(alias_generator=to_camel)
sample_id: int = Field(description='样品ID')
sample_sn: Optional[str] = Field(default=None, description='样品SN号')
reason: str = Field(description='失败原因')
class WorkOrderGenerationResponseModel(BaseModel):
"""
工单生成响应模型
"""
model_config = ConfigDict(alias_generator=to_camel)
success_count: int = Field(description='成功数量')
failed_count: int = Field(description='失败数量')
work_order_ids: List[int] = Field(default_factory=list, description='生成的工单ID列表')
failed_samples: List[FailedSampleInfo] = Field(default_factory=list, description='失败的样品信息')

View File

@ -114,3 +114,249 @@ class WarehouseSampleService:
@classmethod
async def generate_work_orders_from_samples(
cls,
db: AsyncSession,
request_model: 'GenerateWorkOrderFromSampleModel',
current_user_id: int
) -> 'WorkOrderGenerationResponseModel':
"""
从样品生成工单
:param db: 数据库会话
:param request_model: 工单生成请求模型
:param current_user_id: 当前用户ID
:return: 工单生成响应模型
"""
from module_admin.entity.vo.warehouse_sample_vo import (
GenerateWorkOrderFromSampleModel,
WorkOrderGenerationResponseModel,
FailedSampleInfo
)
from module_admin.system.entity.do.test_flow_tags_do import TestFlowTags
from module_admin.system.entity.do.test_item_do import TestItem
from module_admin.system.entity.do.test_job_do import TestJob
from module_admin.system.entity.do.test_order_do import TestOrder
from module_admin.system.entity.do.test_work_order_do import TestWorkOrder
from module_admin.system.entity.do.test_eut_do import TestEut
from sqlalchemy import select
success_count = 0
failed_count = 0
work_order_ids = []
failed_samples = []
# 1. 验证样品ID列表
if not request_model.sample_ids:
raise ServiceException(message='样品ID列表不能为空')
# 2. 验证测试流程ID
test_flow_tags_result = await db.execute(
select(TestFlowTags.test_category_id)
.where(TestFlowTags.test_flow_id == request_model.test_flow_id)
)
test_flow_tags = test_flow_tags_result.all()
print(f"DEBUG: 测试流程ID={request_model.test_flow_id}, 找到 {len(test_flow_tags)} 个测试类别")
if not test_flow_tags:
raise ServiceException(message=f'测试流程ID【{request_model.test_flow_id}】不存在或未配置测试类别')
print(f"DEBUG: 准备创建测试订单")
# 3. 创建测试订单(用于分组工单)
test_order = TestOrder(
name=request_model.work_order_name or f'样品工单-{datetime.now().strftime("%Y%m%d%H%M%S")}',
creator=current_user_id,
create_time=datetime.now(),
update_by=current_user_id,
update_time=datetime.now(),
complate_count=0, # 完成数量初始为0
total_count=len(request_model.sample_ids), # 总数量为样品数量
state=0, # 待测试
memo=request_model.memo
)
print(f"DEBUG: 测试订单对象创建成功")
db.add(test_order)
print(f"DEBUG: 测试订单已添加到session")
await db.flush() # 获取生成的order_id
order_id = test_order.id
print(f"DEBUG: 测试订单已保存ID={order_id}")
# 4. 为每个样品生成工单
for sample_id in request_model.sample_ids:
try:
# 4.1 查询样品信息
sample = await WarehouseSampleDao.get_sample_by_id(db, sample_id)
print(f"DEBUG: 处理样品ID={sample_id}, 样品存在={sample is not None}")
if not sample:
failed_samples.append(FailedSampleInfo(
sampleId=sample_id,
sampleSn=None,
reason='样品不存在'
))
failed_count += 1
continue
print(f"DEBUG: 样品ID={sample_id}, SN={sample.sample_sn}, 状态={sample.status}")
# 4.2 验证样品状态只允许待测试状态的样品除非force=True
if not request_model.force and sample.status != '0':
failed_samples.append(FailedSampleInfo(
sampleId=sample_id,
sampleSn=sample.sample_sn,
reason='已有工单'
))
failed_count += 1
continue
# 4.3 为样品创建test_eut记录如果不存在
# 先查询是否已存在相同SN的test_eut
test_eut_result = await db.execute(
select(TestEut.id)
.where(TestEut.sn == sample.sample_sn)
.limit(1)
)
existing_eut = test_eut_result.first()
if existing_eut:
test_eut_id = existing_eut.id
print(f"DEBUG: 样品SN={sample.sample_sn}已有test_eut记录ID={test_eut_id}")
else:
# 创建新的test_eut记录
test_eut = TestEut(
test_order_id=order_id,
test_flow_id=request_model.test_flow_id,
sn=sample.sample_sn,
version=sample.hardware_version,
memo=sample.remark
)
db.add(test_eut)
await db.flush()
test_eut_id = test_eut.id
print(f"DEBUG: 为样品SN={sample.sample_sn}创建test_eut记录ID={test_eut_id}")
# 4.4 为该样品的每个测试类别创建工单
sample_work_order_count = 0
for tag in test_flow_tags:
test_category_id = tag.test_category_id
print(f"DEBUG: 样品ID={sample_id}, 测试类别ID={test_category_id}")
# 4.4 查询测试单元(根据测试类别和产品类型)
# 注意这里假设样品的sample_model可以映射到eut_type_id
# 如果没有eut_type_id则使用test_category_id查询
test_item_result = await db.execute(
select(TestItem.id, TestItem.name)
.where(TestItem.test_category_id == test_category_id)
.limit(1)
)
test_item = test_item_result.first()
print(f"DEBUG: 测试类别ID={test_category_id}, 找到测试单元={test_item is not None}")
if not test_item:
# 如果没有找到测试单元,记录警告但继续
print(f"WARNING: 测试类别ID={test_category_id} 没有找到测试单元")
continue
# 4.5 查询测试任务获取测试人员信息
test_job_result = await db.execute(
select(
TestJob.id,
TestJob.name,
TestJob.tester_id,
TestJob.reviewer_id,
TestJob.second_tester_id,
TestJob.third_tester_id
).where(TestJob.test_item_id == test_item.id)
.limit(1)
)
test_job = test_job_result.first()
print(f"DEBUG: 测试单元ID={test_item.id}, 找到测试任务={test_job is not None}")
# 4.6 构造工单名称
if request_model.work_order_name:
work_order_name = f"{request_model.work_order_name}-{sample.sample_sn}"
else:
work_order_name = f"{sample.sample_sn}-{test_item.name if test_item else '测试'}"
print(f"DEBUG: 准备创建工单, 名称={work_order_name}")
# 4.7 创建工单直接使用DO对象避免Service层的commit
# 注意某些字段在数据库中定义为NOT NULL必须提供默认值
work_order = TestWorkOrder(
name=work_order_name,
test_order_id=order_id, # 关联测试订单
test_eut_id=test_eut_id, # 关联test_eut表的ID不是样品ID
test_category_id=test_category_id,
test_item_id=test_item.id if test_item else 0, # 如果没有测试单元使用0
creator=current_user_id,
create_time=datetime.now(),
update_by=current_user_id,
update_time=datetime.now(),
test_step=1,
test_status=0,
tester_id=test_job.tester_id if (test_job and test_job.tester_id) else 0,
reviewer_id=test_job.reviewer_id if (test_job and test_job.reviewer_id) else 0,
second_tester_id=test_job.second_tester_id if (test_job and test_job.second_tester_id) else 0,
third_tester_id=test_job.third_tester_id if (test_job and test_job.third_tester_id) else 0,
memo=request_model.memo
)
print(f"DEBUG: 工单对象创建成功,准备保存")
# 保存工单使用db.add而不是Service避免中途commit
db.add(work_order)
await db.flush() # flush但不commit
work_order_id = work_order.id
work_order_ids.append(work_order_id) # 添加到工单ID列表
print(f"DEBUG: 工单保存成功ID={work_order_id}")
sample_work_order_count += 1
# 4.8 如果该样品至少创建了一个工单,更新样品状态为"测试中"
if sample_work_order_count > 0:
# 只有当样品状态为"待测试"时才更新为"测试中"
# 如果样品已经是其他状态(如已在测试中),则保持原状态
if sample.status == '0':
sample.status = '1' # 测试中
sample.update_time = datetime.now()
await db.flush()
success_count += 1
else:
failed_samples.append(FailedSampleInfo(
sampleId=sample_id,
sampleSn=sample.sample_sn,
reason='未找到匹配的测试单元'
))
failed_count += 1
except Exception as e:
# 单个样品失败不影响其他样品
# 截断错误消息只保留前200个字符
error_msg = str(e)
if len(error_msg) > 200:
error_msg = error_msg[:200] + '...'
failed_samples.append(FailedSampleInfo(
sampleId=sample_id,
sampleSn=None,
reason=f'创建工单失败: {error_msg}'
))
failed_count += 1
# 记录完整错误到日志
print(f"ERROR: 样品 {sample_id} 生成工单失败: {str(e)}")
continue
# 5. 提交事务
await db.commit()
# 6. 返回结果
return WorkOrderGenerationResponseModel(
successCount=success_count,
failedCount=failed_count,
workOrderIds=work_order_ids,
failedSamples=failed_samples
)

View File

@ -48,3 +48,12 @@ export function delSample(sampleIds) {
// 从样品生成工单
export function generateWorkOrders(data) {
return request({
url: '/warehouse/sample/generate_work_orders',
method: 'post',
data: data
})
}

View File

@ -78,6 +78,17 @@
v-hasPermi="['warehouse:sample:add']"
>批量添加样品</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-s-order"
size="mini"
:disabled="multiple"
@click="handleGenerateWorkOrder"
v-hasPermi="['warehouse:sample:add']"
>生成工单</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
@ -278,6 +289,48 @@
<el-button @click="batchSampleOpen = false"> </el-button>
</div>
</el-dialog>
<!-- 生成工单对话框 -->
<el-dialog title="生成工单" :visible.sync="workOrderDialogVisible" width="600px" append-to-body :close-on-click-modal="false">
<el-form label-width="120px">
<el-form-item label="选中样品数量">
<el-tag type="info">{{ workOrderForm.sampleIds.length }} 个样品</el-tag>
</el-form-item>
<el-form-item label="测试流程" required>
<el-select v-model="workOrderForm.testFlowId" placeholder="请选择测试流程" style="width: 100%">
<el-option
v-for="item in testFlowOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- 测试流程标签显示 -->
<div v-if="testFlowTags.length" style="margin-top: 10px;">
<el-tag
v-for="(tag, index) in testFlowTags"
:key="tag.test_category_id"
:style="{marginRight: '5px', backgroundColor: tagColors[index % tagColors.length], color: 'white'}"
>
{{ tag.test_category_name || '' }}
</el-tag>
</div>
</el-form-item>
<el-form-item label="工单名称">
<el-input v-model="workOrderForm.workOrderName" placeholder="可选,不填则使用默认命名" />
<div style="margin-top: 5px; color: #909399; font-size: 12px">
<i class="el-icon-info"></i> 提示默认命名规则为"样品SN-测试类别"
</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="workOrderForm.memo" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitWorkOrderGeneration"> </el-button>
<el-button @click="workOrderDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
@ -314,6 +367,21 @@ export default {
batchSampleModel: "",
//
batchHardwareVersion: "",
//
workOrderDialogVisible: false,
//
workOrderForm: {
sampleIds: [],
testFlowId: null,
workOrderName: '',
memo: ''
},
//
testFlowOptions: [],
//
testFlowTags: [],
//
tagColors: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#C0C4CC'],
// ID
receiptId: null,
//
@ -347,6 +415,16 @@ export default {
this.queryParams.receiptId = this.receiptId;
}
this.getList();
this.loadTestFlowOptions();
},
watch: {
'workOrderForm.testFlowId'(newVal) {
if (newVal) {
this.fetchTestFlowTags(newVal);
} else {
this.testFlowTags = [];
}
}
},
methods: {
/** 查询样品列表 */
@ -554,6 +632,167 @@ export default {
// 20
cb(results.slice(0, 20));
},
/** 打开生成工单对话框 */
handleGenerateWorkOrder() {
if (this.ids.length === 0) {
this.$modal.msgWarning("请先选择样品");
return;
}
//
this.workOrderForm = {
sampleIds: [...this.ids],
testFlowId: null,
workOrderName: '',
memo: ''
};
this.testFlowTags = [];
this.workOrderDialogVisible = true;
},
/** 加载测试流程选项 */
async loadTestFlowOptions() {
try {
const { listTest_flow } = await import("@/api/system/test_eut");
const response = await listTest_flow();
this.testFlowOptions = response.data.map(item => ({
label: item.name,
value: item.id
}));
} catch (error) {
console.error('加载测试流程失败:', error);
}
},
/** 获取测试流程标签 */
async fetchTestFlowTags(flowId) {
try {
const { getTest_flow_tags_by_flow_id, listTest_category } = await import("@/api/system/test_order");
//
const tagsResponse = await getTest_flow_tags_by_flow_id(flowId);
//
const categoryResponse = await listTest_category();
const categoryMap = categoryResponse.rows.reduce((map, item) => {
map[item.id] = item.name;
return map;
}, {});
//
this.testFlowTags = tagsResponse.data.map(tag => ({
test_category_id: tag.testCategoryId,
test_category_name: categoryMap[tag.testCategoryId] || ''
}));
} catch (error) {
console.error('获取测试流程标签失败:', error);
this.testFlowTags = [];
}
},
/** 提交工单生成 */
async submitWorkOrderGeneration() {
//
if (!this.workOrderForm.testFlowId) {
this.$modal.msgWarning("请选择测试流程");
return;
}
try {
const { generateWorkOrders } = await import("@/api/warehouse/sample");
//
const response = await generateWorkOrders(this.workOrderForm);
if (response.code === 200) {
const result = response.data;
//
if (result.successCount === 0 && result.failedCount > 0) {
const hasStatusIssue = result.failedSamples.some(s =>
s.reason.includes('已有工单') || s.reason.includes('状态不允许') || s.reason.includes('如需重复生成请确认')
);
if (hasStatusIssue) {
// SN
const failedSns = result.failedSamples
.map(s => s.sampleSn || '样品' + s.sampleId)
.join('、');
//
this.$confirm(
`以下样品已有工单:${failedSns},是否重新生成?`,
'确认',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
//
const forceForm = { ...this.workOrderForm, force: true };
const forceResponse = await generateWorkOrders(forceForm);
if (forceResponse.code === 200) {
const forceResult = forceResponse.data;
//
if (forceResult.failedCount === 0) {
this.$modal.msgSuccess(`成功为 ${forceResult.successCount} 个样品生成工单`);
} else if (forceResult.successCount === 0) {
this.$modal.msgError(`工单生成失败,${forceResult.failedCount} 个样品处理失败`);
} else {
this.$modal.msgWarning(`部分成功:${forceResult.successCount} 个样品成功,${forceResult.failedCount} 个样品失败`);
}
//
if (forceResult.failedSamples && forceResult.failedSamples.length > 0) {
const failedInfo = forceResult.failedSamples.map(s =>
`样品${s.sampleSn || s.sampleId}: ${s.reason}`
).join('\n');
console.log('失败详情:\n' + failedInfo);
}
//
this.workOrderDialogVisible = false;
this.getList();
} else {
this.$modal.msgError(forceResponse.msg || '生成工单失败');
}
}).catch(() => {
//
this.$modal.msgInfo('已取消');
});
return; //
}
}
//
if (result.failedCount === 0) {
this.$modal.msgSuccess(`成功为 ${result.successCount} 个样品生成工单`);
} else if (result.successCount === 0) {
this.$modal.msgError(`工单生成失败,${result.failedCount} 个样品处理失败`);
} else {
this.$modal.msgWarning(`部分成功:${result.successCount} 个样品成功,${result.failedCount} 个样品失败`);
}
//
if (result.failedSamples && result.failedSamples.length > 0) {
const failedInfo = result.failedSamples.map(s =>
`样品${s.sampleSn || s.sampleId}: ${s.reason}`
).join('\n');
console.log('失败详情:\n' + failedInfo);
}
//
this.workOrderDialogVisible = false;
this.getList();
} else {
this.$modal.msgError(response.msg || '生成工单失败');
}
} catch (error) {
console.error('生成工单失败:', error);
this.$modal.msgError('生成工单失败: ' + (error.message || '未知错误'));
}
}
}
};