From 6fd199a218c676bb78a181322cf83a115ec188d4 Mon Sep 17 00:00:00 2001 From: colaftc Date: Mon, 17 Nov 2025 16:37:21 +0800 Subject: [PATCH] feat: query all process nodes for business_object --- CHANGELOG_process_nodes.md | 177 ++++++++ api_v1/views/stateflow/business_object.py | 23 ++ api_v1/views/upload/API.md | 425 ++++++++++++++++++++ debug_advance_bug.py | 154 +++++++ examples/test_process_nodes_api.py | 35 ++ stateflow/API.md | 65 ++- stateflow/services.py | 31 ++ stateflow/tests/test_business_object_api.py | 36 ++ stateflow/tests/test_services.py | 30 ++ 9 files changed, 973 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG_process_nodes.md create mode 100644 api_v1/views/upload/API.md create mode 100644 debug_advance_bug.py create mode 100644 examples/test_process_nodes_api.py diff --git a/CHANGELOG_process_nodes.md b/CHANGELOG_process_nodes.md new file mode 100644 index 0000000..c8fb238 --- /dev/null +++ b/CHANGELOG_process_nodes.md @@ -0,0 +1,177 @@ +# 新增功能:获取业务对象流程节点接口 + +## 概述 + +为 Stateflow 模块新增了获取业务对象所属流程的所有状态节点的功能。 + +## 变更内容 + +### 1. Services 层 (`stateflow/services.py`) + +新增函数 `get_process_nodes(business_object)`: + +```python +def get_process_nodes(business_object: 'models.BusinessObject') -> List[dict]: + """ + 获取业务对象所属流程的所有状态节点列表 + + 参数: + business_object: BusinessObject 实例 + + 返回: + 节点列表,每个元素包含: + { + 'id': ProcessNode ID, + 'state': State对象, + 'state_id': State ID, + 'state_name': State 名称, + 'order': 节点顺序号 + } + """ +``` + +**用途**: 获取完整的流程节点列表,包含节点顺序和状态信息。 + +### 2. API 层 (`api_v1/views/stateflow/business_object.py`) + +新增 API 端点: + +- **URL**: `GET /api/v1/stateflow/business-objects/{id}/process-nodes/` +- **描述**: 获取指定业务对象所属流程的所有状态节点 +- **响应格式**: + +```json +{ + "count": 3, + "nodes": [ + { + "id": 1, + "state_id": 10, + "state_name": "质检", + "order": 0 + }, + { + "id": 2, + "state_id": 11, + "state_name": "包装", + "order": 1 + }, + { + "id": 3, + "state_id": 12, + "state_name": "发货", + "order": 2 + } + ] +} +``` + +### 3. 测试覆盖 + +#### 功能测试 (`stateflow/tests/test_services.py`) + +新增测试: `test_get_process_nodes` + +- ✅ 验证节点数量正确 +- ✅ 验证节点顺序正确 +- ✅ 验证节点字段完整性 +- ✅ 验证每个节点包含必要字段 + +#### API 测试 (`stateflow/tests/test_business_object_api.py`) + +新增测试: `test_get_process_nodes` + +- ✅ 验证 HTTP 200 响应 +- ✅ 验证响应包含 count 和 nodes 字段 +- ✅ 验证节点数据格式正确 +- ✅ 验证节点顺序和内容准确 + +### 4. 文档更新 + +#### API 文档 (`stateflow/API.md`) + +新增章节: **3.7.5. 获取流程的所有节点** + +- 📝 完整的接口说明 +- 📝 请求/响应示例 +- 📝 Python/JavaScript/curl 使用示例 + +## 测试结果 + +```bash +# 功能测试 +$ python manage.py test stateflow.tests.test_services.StateFlowServicesTestCase.test_get_process_nodes +✅ OK + +# API 测试 +$ python manage.py test stateflow.tests.test_business_object_api.BusinessObjectAPITestCase.test_get_process_nodes +✅ OK + +# 完整测试套件 +$ python manage.py test stateflow +✅ Ran 80 tests in 11.122s - OK +``` + +## 使用示例 + +### Python + +```python +import requests + +headers = {'Authorization': f'Bearer {token}'} +response = requests.get( + 'http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/', + headers=headers +) +data = response.json() + +print(f"流程共有 {data['count']} 个节点:") +for node in data['nodes']: + print(f" 节点 {node['order']}: {node['state_name']}") +``` + +### JavaScript + +```javascript +const response = await fetch( + `/api/v1/stateflow/business-objects/${businessObjectId}/process-nodes/`, + { headers: { 'Authorization': `Bearer ${token}` } } +); +const data = await response.json(); +console.log(`流程共有 ${data.count} 个节点:`, data.nodes); +``` + +### curl + +```bash +curl -X GET "http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/" \ + -H "Authorization: Bearer " +``` + +## 应用场景 + +1. **前端流程可视化**: 获取完整的流程节点列表用于渲染流程图 +2. **进度追踪**: 结合 timeline 接口展示完整的流程进度 +3. **流程分析**: 分析流程包含的所有节点和顺序 +4. **文档生成**: 自动生成流程说明文档 + +## 兼容性 + +- ✅ 完全向后兼容 +- ✅ 不影响现有 API +- ✅ 所有现有测试通过 (80/80) +- ✅ 新增测试覆盖完整 + +## 相关文件 + +- `stateflow/services.py` - 新增 `get_process_nodes()` 函数 +- `api_v1/views/stateflow/business_object.py` - 新增 `process_nodes()` action +- `stateflow/tests/test_services.py` - 新增功能测试 +- `stateflow/tests/test_business_object_api.py` - 新增 API 测试 +- `stateflow/API.md` - 更新文档 +- `examples/test_process_nodes_api.py` - 使用示例 + +## 完成日期 + +2025-11-17 diff --git a/api_v1/views/stateflow/business_object.py b/api_v1/views/stateflow/business_object.py index 1454d32..fa814fd 100644 --- a/api_v1/views/stateflow/business_object.py +++ b/api_v1/views/stateflow/business_object.py @@ -285,6 +285,29 @@ class BusinessObjectViewSet(viewsets.ModelViewSet): 'count': len(parameters) }) + @action(detail=True, methods=['get'], url_path='process-nodes') + def process_nodes(self, request, pk=None): + """ + 获取业务对象所属流程的所有状态节点 + + 返回流程中所有节点的列表,按顺序排列 + """ + business_object = self.get_object() + nodes = services.get_process_nodes(business_object) + + return Response({ + 'count': len(nodes), + 'nodes': [ + { + 'id': node['id'], + 'state_id': node['state_id'], + 'state_name': node['state_name'], + 'order': node['order'] + } + for node in nodes + ] + }) + @action(detail=True, methods=['post'], url_path='state-logs/(?P[^/.]+)/add-parameters') def add_parameters_to_log(self, request, pk=None, log_id=None): """ diff --git a/api_v1/views/upload/API.md b/api_v1/views/upload/API.md new file mode 100644 index 0000000..41f23f8 --- /dev/null +++ b/api_v1/views/upload/API.md @@ -0,0 +1,425 @@ +# 文件上传接口文档 + +## 概述 + +通用文件上传接口,用于上传无法归类到具体业务的文件。 + +**基础路径**: `/api/v1/upload/` + +**认证要求**: 所有接口都需要 JWT Token 认证 + +**内容格式**: `multipart/form-data` (上传时) / `application/json` (响应) + +**注意事项**: +- 不支持列表查询(list) +- 不支持修改操作(PUT/PATCH) +- 仅支持单个文件查询、上传、删除操作 + +--- + +## 数据模型 + +### UploadedFile + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | integer | 文件ID | +| path | string | 文件存储路径(随机文件名) | +| file_url | string | 文件访问URL | +| owner | integer | 上传者用户ID | +| owner_username | string | 上传者用户名 | +| is_deleted | boolean | 是否已删除(软删除标记) | +| original_filename | string | 原始文件名 | +| file_size | integer | 文件大小(字节) | +| content_type | string | MIME类型(如 image/jpeg) | +| created_at | datetime | 创建时间 | +| updated_at | datetime | 更新时间 | + +--- + +## 接口列表 + +### 1. 上传文件 + +**请求** +``` +POST /api/v1/upload/ +Content-Type: multipart/form-data +Authorization: Bearer +``` + +**请求参数** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| file | file | 是 | 要上传的文件(最大100MB) | + +**请求示例** +```bash +curl -X POST http://localhost:8000/api/v1/upload/ \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -F "file=@/path/to/your/file.pdf" +``` + +**成功响应** (201 Created) +```json +{ + "id": 1, + "path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf", + "file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf", + "owner": 1, + "owner_username": "admin", + "is_deleted": false, + "original_filename": "document.pdf", + "file_size": 1048576, + "content_type": "application/pdf", + "created_at": "2025-11-17T10:30:00Z", + "updated_at": "2025-11-17T10:30:00Z" +} +``` + +**错误响应** (400 Bad Request) +```json +{ + "file": ["未上传文件"] +} +``` + +```json +{ + "file": ["文件大小不能超过100MB"] +} +``` + +**安全特性**: +- 文件名使用 UUID 随机化,防止文件名冲突和路径遍历攻击 +- 原始文件名保存在数据库中,不影响存储安全 +- 自动记录上传者信息 + +--- + +### 2. 获取文件信息 + +**请求** +``` +GET /api/v1/upload/{id}/ +Authorization: Bearer +``` + +**路径参数** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | integer | 是 | 文件ID | + +**请求示例** +```bash +curl -X GET http://localhost:8000/api/v1/upload/1/ \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**成功响应** (200 OK) +```json +{ + "id": 1, + "path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf", + "file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf", + "owner": 1, + "owner_username": "admin", + "is_deleted": false, + "original_filename": "document.pdf", + "file_size": 1048576, + "content_type": "application/pdf", + "created_at": "2025-11-17T10:30:00Z", + "updated_at": "2025-11-17T10:30:00Z" +} +``` + +**错误响应** (404 Not Found) +```json +{ + "detail": "未找到" +} +``` + +**注意**: 已软删除的文件无法通过此接口查询 + +--- + +### 3. 软删除文件 + +**请求** +``` +DELETE /api/v1/upload/{id}/ +Authorization: Bearer +``` + +**路径参数** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | integer | 是 | 文件ID | + +**请求示例** +```bash +curl -X DELETE http://localhost:8000/api/v1/upload/1/ \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**成功响应** (200 OK) +```json +{ + "detail": "文件已标记为删除" +} +``` + +**说明**: +- 软删除不会物理删除文件,只是标记为已删除 +- 软删除后的文件无法通过常规接口查询 +- 可以通过恢复接口恢复文件 + +--- + +### 4. 恢复已删除文件 + +**请求** +``` +POST /api/v1/upload/{id}/restore/ +Authorization: Bearer +``` + +**路径参数** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | integer | 是 | 文件ID | + +**请求示例** +```bash +curl -X POST http://localhost:8000/api/v1/upload/1/restore/ \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**成功响应** (200 OK) +```json +{ + "detail": "文件已恢复", + "data": { + "id": 1, + "path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf", + "file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf", + "owner": 1, + "owner_username": "admin", + "is_deleted": false, + "original_filename": "document.pdf", + "file_size": 1048576, + "content_type": "application/pdf", + "created_at": "2025-11-17T10:30:00Z", + "updated_at": "2025-11-17T10:30:00Z" + } +} +``` + +**错误响应** (400 Bad Request) +```json +{ + "detail": "文件未被删除,无需恢复" +} +``` + +--- + +### 5. 永久删除文件 + +**请求** +``` +DELETE /api/v1/upload/{id}/permanent_delete/ +Authorization: Bearer +``` + +**路径参数** +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | integer | 是 | 文件ID | + +**请求示例** +```bash +curl -X DELETE http://localhost:8000/api/v1/upload/1/permanent_delete/ \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**成功响应** (204 No Content) +```json +{ + "detail": "文件已永久删除" +} +``` + +**说明**: +- 永久删除会物理删除文件和数据库记录 +- 此操作不可恢复,请谨慎使用 +- 建议仅在确认不需要时使用 + +--- + +## 使用示例 + +### Python (requests) + +```python +import requests + +# 配置 +BASE_URL = "http://localhost:8000/api/v1" +TOKEN = "your_jwt_token" +headers = {"Authorization": f"Bearer {TOKEN}"} + +# 1. 上传文件 +with open('document.pdf', 'rb') as f: + files = {'file': f} + response = requests.post( + f"{BASE_URL}/upload/", + headers=headers, + files=files + ) + file_data = response.json() + file_id = file_data['id'] + print(f"上传成功,文件ID: {file_id}") + +# 2. 获取文件信息 +response = requests.get( + f"{BASE_URL}/upload/{file_id}/", + headers=headers +) +print(f"文件信息: {response.json()}") + +# 3. 软删除文件 +response = requests.delete( + f"{BASE_URL}/upload/{file_id}/", + headers=headers +) +print(f"软删除: {response.json()}") + +# 4. 恢复文件 +response = requests.post( + f"{BASE_URL}/upload/{file_id}/restore/", + headers=headers +) +print(f"恢复文件: {response.json()}") + +# 5. 永久删除 +response = requests.delete( + f"{BASE_URL}/upload/{file_id}/permanent_delete/", + headers=headers +) +print(f"永久删除完成") +``` + +### JavaScript (Axios) + +```javascript +const axios = require('axios'); +const FormData = require('form-data'); +const fs = require('fs'); + +const BASE_URL = 'http://localhost:8000/api/v1'; +const TOKEN = 'your_jwt_token'; +const headers = { Authorization: `Bearer ${TOKEN}` }; + +// 1. 上传文件 +async function uploadFile() { + const formData = new FormData(); + formData.append('file', fs.createReadStream('document.pdf')); + + const response = await axios.post( + `${BASE_URL}/upload/`, + formData, + { headers: { ...headers, ...formData.getHeaders() } } + ); + + console.log('上传成功:', response.data); + return response.data.id; +} + +// 2. 获取文件信息 +async function getFileInfo(fileId) { + const response = await axios.get( + `${BASE_URL}/upload/${fileId}/`, + { headers } + ); + console.log('文件信息:', response.data); +} + +// 3. 软删除 +async function softDelete(fileId) { + const response = await axios.delete( + `${BASE_URL}/upload/${fileId}/`, + { headers } + ); + console.log('软删除:', response.data); +} + +// 4. 恢复文件 +async function restore(fileId) { + const response = await axios.post( + `${BASE_URL}/upload/${fileId}/restore/`, + {}, + { headers } + ); + console.log('恢复:', response.data); +} + +// 5. 永久删除 +async function permanentDelete(fileId) { + const response = await axios.delete( + `${BASE_URL}/upload/${fileId}/permanent_delete/`, + { headers } + ); + console.log('永久删除完成'); +} +``` + +--- + +## 错误码说明 + +| HTTP状态码 | 说明 | +|-----------|------| +| 200 | 成功 | +| 201 | 创建成功 | +| 204 | 删除成功(无内容) | +| 400 | 请求参数错误 | +| 401 | 未认证或认证失败 | +| 403 | 无权限 | +| 404 | 资源不存在 | +| 413 | 文件过大 | +| 500 | 服务器内部错误 | + +--- + +## 最佳实践 + +1. **文件大小限制**: 单文件最大100MB,超过此限制会返回400错误 +2. **文件命名**: 系统自动使用UUID生成随机文件名,原始文件名保存在`original_filename`字段 +3. **软删除策略**: 建议先使用软删除,确认不需要后再使用永久删除 +4. **文件访问**: 使用返回的`file_url`字段访问文件 +5. **权限控制**: 所有接口都需要认证,上传的文件自动关联当前用户 + +--- + +## 注意事项 + +1. **不支持的操作**: + - ❌ 列表查询 (`GET /api/v1/upload/`) + - ❌ 批量上传 + - ❌ 修改文件 (`PUT/PATCH /api/v1/upload/{id}/`) + +2. **文件存储**: + - 文件按日期组织:`uploads/YYYY/MM/DD/` + - 文件名使用32位十六进制UUID + - 保留原始文件扩展名 + +3. **查询限制**: + - 默认查询会过滤掉已软删除的文件 + - 要访问已删除文件,需要通过Django Admin或直接数据库查询 + +4. **安全考虑**: + - 所有文件名随机化,防止路径遍历攻击 + - 需要JWT认证 + - 自动记录上传者信息 diff --git a/debug_advance_bug.py b/debug_advance_bug.py new file mode 100644 index 0000000..3fa9c4c --- /dev/null +++ b/debug_advance_bug.py @@ -0,0 +1,154 @@ +""" +调试脚本:检查 advance_to_next_state 是否会导致"前进2步"的bug +""" +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'flower.settings') +django.setup() + +from django.contrib.auth import get_user_model +from stateflow.models import State, Process, BusinessObject, StateFlowRecord +from stateflow.services import advance_to_next_state, get_business_object_current_state, get_completed_node_ids, get_progress_percentage + +User = get_user_model() + +def main(): + # 创建测试用户 + user = User.objects.first() + if not user: + user = User.objects.create_user(username='debuguser', password='debug123') + print(f"创建测试用户: {user.username}") + + # 创建3个状态节点 + state1 = State.objects.create(name='Debug State 1') + state2 = State.objects.create(name='Debug State 2') + state3 = State.objects.create(name='Debug State 3') + print(f"\n创建状态节点:") + print(f" - {state1.name} (ID: {state1.id})") + print(f" - {state2.name} (ID: {state2.id})") + print(f" - {state3.name} (ID: {state3.id})") + + # 创建流程 + process = Process.objects.create(name='Debug Process') + process.replace_nodes([state1, state2, state3]) + print(f"\n创建流程: {process.name}") + print(f"节点顺序: {state1.name} -> {state2.name} -> {state3.name}") + + # 创建业务对象 + bo = BusinessObject.objects.create(name='Debug BO', process=process) + print(f"\n创建业务对象: {bo.name}") + + # 检查初始状态 + print(f"\n{'='*60}") + print(f"初始状态检查") + print(f"{'='*60}") + current = get_business_object_current_state(bo) + completed_ids = get_completed_node_ids(bo) + progress = get_progress_percentage(bo) + logs_count = StateFlowRecord.objects.filter(business_object=bo, is_cancelled=False).count() + + print(f"Current State: {current.name if current else 'None'} (ID: {current.id if current else 'N/A'})") + print(f"Completed Node IDs: {completed_ids}") + print(f"Progress: {progress:.2f}%") + print(f"Log Records Count: {logs_count}") + print(f"\n✓ 期望: Current='{state1.name}', Completed=[], Progress=0%, Logs=0") + + if current and current.id != state1.id: + print(f"❌ 错误: Current State 应该是 '{state1.name}' 但实际是 '{current.name}'") + cleanup(bo, process, [state1, state2, state3]) + return False + + if len(completed_ids) != 0: + print(f"❌ 错误: 应该没有完成的节点,但有 {len(completed_ids)} 个") + cleanup(bo, process, [state1, state2, state3]) + return False + + print("✅ 初始状态正确") + + # 第一次推进 + print(f"\n{'='*60}") + print(f"第一次调用 advance_to_next_state") + print(f"{'='*60}") + + success, message, log = advance_to_next_state(bo, user) + + print(f"Success: {success}") + print(f"Message: {message}") + print(f"Created Log ID: {log.id if log else 'None'}") + if log: + print(f"Log State: {log.state.name} (ID: {log.state_id})") + + # 检查推进后的状态 + print(f"\n推进后状态检查:") + current = get_business_object_current_state(bo) + completed_ids = get_completed_node_ids(bo) + progress = get_progress_percentage(bo) + logs_count = StateFlowRecord.objects.filter(business_object=bo, is_cancelled=False).count() + all_logs = StateFlowRecord.objects.filter(business_object=bo, is_cancelled=False).values_list('state_id', 'state__name') + + print(f"Current State: {current.name if current else 'None'} (ID: {current.id if current else 'N/A'})") + print(f"Completed Node IDs: {completed_ids}") + print(f"Completed Nodes: {[State.objects.get(id=cid).name for cid in completed_ids]}") + print(f"Progress: {progress:.2f}%") + print(f"Log Records Count: {logs_count}") + print(f"All Logs: {list(all_logs)}") + + print(f"\n✓ 期望: Current='{state2.name}', Completed=[{state1.name}], Progress=33.33%, Logs=1") + + # 验证 + has_error = False + + if current and current.id != state2.id: + print(f"\n❌ BUG FOUND! Current State 应该是 '{state2.name}' (ID: {state2.id}) 但实际是 '{current.name}' (ID: {current.id})") + has_error = True + + if len(completed_ids) != 1: + print(f"\n❌ BUG FOUND! 应该完成1个节点,但实际完成了 {len(completed_ids)} 个") + has_error = True + + if completed_ids and completed_ids[0] != state1.id: + print(f"\n❌ BUG FOUND! 完成的节点应该是 '{state1.name}' 但实际是 ID {completed_ids[0]}") + has_error = True + + if logs_count != 1: + print(f"\n❌ BUG FOUND! 应该有1条日志记录,但实际有 {logs_count} 条") + has_error = True + + if abs(progress - 33.33) > 0.1: + print(f"\n❌ BUG FOUND! 进度应该是 33.33% 但实际是 {progress:.2f}%") + has_error = True + + if not has_error: + print("\n✅ 所有检查通过!没有发现bug") + + # 清理 + cleanup(bo, process, [state1, state2, state3]) + + return not has_error + +def cleanup(bo, process, states): + """清理测试数据""" + print(f"\n{'='*60}") + print("清理测试数据...") + bo.delete() + process.delete() + for state in states: + state.delete() + print("清理完成") + print(f"{'='*60}") + +if __name__ == '__main__': + try: + result = main() + if result: + print("\n🎉 测试通过!") + exit(0) + else: + print("\n💥 测试失败!发现bug") + exit(1) + except Exception as e: + print(f"\n❌ 测试异常: {e}") + import traceback + traceback.print_exc() + exit(1) diff --git a/examples/test_process_nodes_api.py b/examples/test_process_nodes_api.py new file mode 100644 index 0000000..ff525ad --- /dev/null +++ b/examples/test_process_nodes_api.py @@ -0,0 +1,35 @@ +""" +测试新增的 get_process_nodes API 端点 +""" +import requests + +BASE_URL = "http://localhost:8000" + +# 假设你已经有了 JWT token +TOKEN = "your_jwt_token_here" +headers = {"Authorization": f"Bearer {TOKEN}"} + +# 业务对象 ID +business_object_id = 1 + +# 获取流程的所有节点 +response = requests.get( + f"{BASE_URL}/api/v1/stateflow/business-objects/{business_object_id}/process-nodes/", + headers=headers +) + +if response.status_code == 200: + data = response.json() + print(f"流程共有 {data['count']} 个节点:") + print() + + for node in data['nodes']: + print(f"节点 {node['order'] + 1}:") + print(f" - ProcessNode ID: {node['id']}") + print(f" - State ID: {node['state_id']}") + print(f" - State 名称: {node['state_name']}") + print(f" - 顺序: {node['order']}") + print() +else: + print(f"错误: {response.status_code}") + print(response.json()) diff --git a/stateflow/API.md b/stateflow/API.md index 5783410..2698976 100644 --- a/stateflow/API.md +++ b/stateflow/API.md @@ -268,6 +268,37 @@ Authorization: Bearer - **描述**: 获取当前待执行节点(`current_state`)的参数模板。 - **注意**: `current_state` 指的是下一个待执行的节点。如果流程已完成,则返回空。 +#### 3.7.5. 获取流程的所有节点 + +- **GET** `/api/v1/stateflow/business-objects/{id}/process-nodes/` +- **描述**: 获取业务对象所属流程的所有状态节点列表,按顺序排列。 +- **响应示例**: + ```json + { + "count": 3, + "nodes": [ + { + "id": 1, + "state_id": 10, + "state_name": "质检", + "order": 0 + }, + { + "id": 2, + "state_id": 11, + "state_name": "包装", + "order": 1 + }, + { + "id": 3, + "state_id": 12, + "state_name": "发货", + "order": 2 + } + ] + } + ``` + ### 3.8. 参数与日志接口 (Custom Actions) #### 3.8.1. 获取状态流转记录列表 @@ -737,7 +768,7 @@ token = response.json()['access'] # 创建状态 headers = {'Authorization': f'Bearer {token}'} response = requests.post( - 'http://localhost:8000/api/v1/states/', + 'http://localhost:8000/api/v1/stateflow/states/', json={ 'name': '待审核', 'description': '等待审核' @@ -745,26 +776,50 @@ response = requests.post( headers=headers ) state = response.json() + +# 获取业务对象的流程节点 +business_object_id = 1 +response = requests.get( + f'http://localhost:8000/api/v1/stateflow/business-objects/{business_object_id}/process-nodes/', + headers=headers +) +nodes_data = response.json() +print(f"流程共有 {nodes_data['count']} 个节点") +for node in nodes_data['nodes']: + print(f" 节点 {node['order']}: {node['state_name']}") ``` ### JavaScript (fetch) ```javascript // 获取状态列表 -const response = await fetch('/api/v1/states/?limit=10&offset=0', { +const response = await fetch('/api/v1/stateflow/states/?limit=10&offset=0', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); console.log(data.results); + +// 获取业务对象的流程节点 +const businessObjectId = 1; +const nodesResponse = await fetch( + `/api/v1/stateflow/business-objects/${businessObjectId}/process-nodes/`, + { + headers: { + 'Authorization': `Bearer ${token}` + } + } +); +const nodesData = await nodesResponse.json(); +console.log(`流程共有 ${nodesData.count} 个节点:`, nodesData.nodes); ``` ### curl ```bash # 创建流程 -curl -X POST "http://localhost:8000/api/v1/processes/" \ +curl -X POST "http://localhost:8000/api/v1/stateflow/processes/" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ @@ -775,4 +830,8 @@ curl -X POST "http://localhost:8000/api/v1/processes/" \ {"state_id": 2, "order": 1} ] }' + +# 获取业务对象的流程节点 +curl -X GET "http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/" \ + -H "Authorization: Bearer " ``` diff --git a/stateflow/services.py b/stateflow/services.py index 94f8d30..e56ff7f 100644 --- a/stateflow/services.py +++ b/stateflow/services.py @@ -476,3 +476,34 @@ def add_parameters_to_state_log(state_log: 'models.StateFlowRecord', remark: str StateLogParameterRecord: 创建的参数记录 """ return create_parameter_record(state_log, remark, **parameters) + + +def get_process_nodes(business_object: 'models.BusinessObject') -> List[dict]: + """ + 获取业务对象所属流程的所有状态节点列表 + + 参数: + business_object: BusinessObject 实例 + + 返回: + 节点列表,每个元素包含: + { + 'id': ProcessNode ID, + 'state': State对象, + 'state_id': State ID, + 'state_name': State 名称, + 'order': 节点顺序号 + } + """ + process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id') + + return [ + { + 'id': node.id, + 'state': node.state, + 'state_id': node.state_id, + 'state_name': node.state.name, + 'order': node.order, + } + for node in process_nodes + ] diff --git a/stateflow/tests/test_business_object_api.py b/stateflow/tests/test_business_object_api.py index 6028142..202f505 100644 --- a/stateflow/tests/test_business_object_api.py +++ b/stateflow/tests/test_business_object_api.py @@ -652,3 +652,39 @@ class BusinessObjectAPITestCase(TestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertFalse(response.data['success']) self.assertIn('流程没有任何节点', response.data['message']) + + def test_get_process_nodes(self): + """测试获取业务对象所属流程的所有状态节点 API""" + response = self.client.get( + f'/api/v1/stateflow/business-objects/{self.business_object.id}/process-nodes/' + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('count', response.data) + self.assertIn('nodes', response.data) + + # 验证节点数量 + self.assertEqual(response.data['count'], 3) + self.assertEqual(len(response.data['nodes']), 3) + + # 验证节点内容和顺序 + nodes = response.data['nodes'] + + self.assertEqual(nodes[0]['state_id'], self.state1.id) + self.assertEqual(nodes[0]['state_name'], self.state1.name) + self.assertEqual(nodes[0]['order'], 0) + + self.assertEqual(nodes[1]['state_id'], self.state2.id) + self.assertEqual(nodes[1]['state_name'], self.state2.name) + self.assertEqual(nodes[1]['order'], 1) + + self.assertEqual(nodes[2]['state_id'], self.state3.id) + self.assertEqual(nodes[2]['state_name'], self.state3.name) + self.assertEqual(nodes[2]['order'], 2) + + # 验证每个节点都有必要的字段 + for node in nodes: + self.assertIn('id', node) + self.assertIn('state_id', node) + self.assertIn('state_name', node) + self.assertIn('order', node) diff --git a/stateflow/tests/test_services.py b/stateflow/tests/test_services.py index bad61c7..277a54b 100644 --- a/stateflow/tests/test_services.py +++ b/stateflow/tests/test_services.py @@ -288,3 +288,33 @@ class StateFlowServicesTestCase(TestCase): self.state3.parameters.add(required_param) params = services.get_state_parameters(self.state3, required_only=True) self.assertEqual(len(params), 1) + + def test_get_process_nodes(self): + """测试获取业务对象所属流程的所有状态节点""" + # 获取流程节点 + nodes = services.get_process_nodes(self.business_object) + + # 验证节点数量 + self.assertEqual(len(nodes), 3) + + # 验证节点顺序和内容 + self.assertEqual(nodes[0]['state_id'], self.state1.id) + self.assertEqual(nodes[0]['state_name'], self.state1.name) + self.assertEqual(nodes[0]['order'], 0) + + self.assertEqual(nodes[1]['state_id'], self.state2.id) + self.assertEqual(nodes[1]['state_name'], self.state2.name) + self.assertEqual(nodes[1]['order'], 1) + + self.assertEqual(nodes[2]['state_id'], self.state3.id) + self.assertEqual(nodes[2]['state_name'], self.state3.name) + self.assertEqual(nodes[2]['order'], 2) + + # 验证每个节点都有必要的字段 + for node in nodes: + self.assertIn('id', node) + self.assertIn('state', node) + self.assertIn('state_id', node) + self.assertIn('state_name', node) + self.assertIn('order', node) + self.assertIsInstance(node['state'], models.State)