diff --git a/.kiro/specs/sample-to-workorder/design.md b/.kiro/specs/sample-to-workorder/design.md new file mode 100644 index 0000000..d71d706 --- /dev/null +++ b/.kiro/specs/sample-to-workorder/design.md @@ -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调用和响应处理 diff --git a/.kiro/specs/sample-to-workorder/requirements.md b/.kiro/specs/sample-to-workorder/requirements.md new file mode 100644 index 0000000..be13d36 --- /dev/null +++ b/.kiro/specs/sample-to-workorder/requirements.md @@ -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 工单应关联样品ID(test_eut_id字段) +3. WHEN 系统创建工单 THEN 工单应关联测试类别ID(test_category_id字段) +4. WHEN 系统创建工单 THEN 工单应关联测试单元ID(test_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 + 测试类别) diff --git a/.kiro/specs/sample-to-workorder/tasks.md b/.kiro/specs/sample-to-workorder/tasks.md new file mode 100644 index 0000000..5628b49 --- /dev/null +++ b/.kiro/specs/sample-to-workorder/tasks.md @@ -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. 最终检查点 + - 确保所有测试通过 + - 验证前后端集成正常 + - 检查用户体验流畅性 + - 如有问题,询问用户 diff --git a/ruoyi-fastapi-backend/module_admin/controller/warehouse_sample_controller.py b/ruoyi-fastapi-backend/module_admin/controller/warehouse_sample_controller.py index c7fa713..c160d70 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/warehouse_sample_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/warehouse_sample_controller.py @@ -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)}') diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/warehouse_sample_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/warehouse_sample_vo.py index 2cc89b1..5af0b25 100644 --- a/ruoyi-fastapi-backend/module_admin/entity/vo/warehouse_sample_vo.py +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/warehouse_sample_vo.py @@ -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='失败的样品信息') diff --git a/ruoyi-fastapi-backend/module_admin/service/warehouse_sample_service.py b/ruoyi-fastapi-backend/module_admin/service/warehouse_sample_service.py index 8066825..8c918c3 100644 --- a/ruoyi-fastapi-backend/module_admin/service/warehouse_sample_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/warehouse_sample_service.py @@ -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 + ) diff --git a/ruoyi-fastapi-frontend/src/api/warehouse/sample.js b/ruoyi-fastapi-frontend/src/api/warehouse/sample.js index 594641f..25523c6 100644 --- a/ruoyi-fastapi-frontend/src/api/warehouse/sample.js +++ b/ruoyi-fastapi-frontend/src/api/warehouse/sample.js @@ -48,3 +48,12 @@ export function delSample(sampleIds) { + +// 从样品生成工单 +export function generateWorkOrders(data) { + return request({ + url: '/warehouse/sample/generate_work_orders', + method: 'post', + data: data + }) +} diff --git a/ruoyi-fastapi-frontend/src/views/warehouse/sample/index.vue b/ruoyi-fastapi-frontend/src/views/warehouse/sample/index.vue index 2754277..0ef917a 100644 --- a/ruoyi-fastapi-frontend/src/views/warehouse/sample/index.vue +++ b/ruoyi-fastapi-frontend/src/views/warehouse/sample/index.vue @@ -78,6 +78,17 @@ v-hasPermi="['warehouse:sample:add']" >批量添加样品 + + 生成工单 + 取 消 + + + + + + {{ workOrderForm.sampleIds.length }} 个样品 + + + + + + + + + {{ tag.test_category_name || '' }} + + + + + + + 提示:默认命名规则为"样品SN-测试类别" + + + + + + + + @@ -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 || '未知错误')); + } } } };