1
0
forked from erp-dev/erp

feat: query all process nodes for business_object

This commit is contained in:
2025-11-17 16:37:21 +08:00
parent d23db848ec
commit 6fd199a218
9 changed files with 973 additions and 3 deletions

177
CHANGELOG_process_nodes.md Normal file
View File

@@ -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 <token>"
```
## 应用场景
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

View File

@@ -285,6 +285,29 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
'count': len(parameters) '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<log_id>[^/.]+)/add-parameters') @action(detail=True, methods=['post'], url_path='state-logs/(?P<log_id>[^/.]+)/add-parameters')
def add_parameters_to_log(self, request, pk=None, log_id=None): def add_parameters_to_log(self, request, pk=None, log_id=None):
""" """

425
api_v1/views/upload/API.md Normal file
View File

@@ -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 <token>
```
**请求参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 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 <token>
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 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 <token>
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 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 <token>
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 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 <token>
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 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认证
- 自动记录上传者信息

154
debug_advance_bug.py Normal file
View File

@@ -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)

View File

@@ -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())

View File

@@ -268,6 +268,37 @@ Authorization: Bearer <access_token>
- **描述**: 获取当前待执行节点(`current_state`)的参数模板。 - **描述**: 获取当前待执行节点(`current_state`)的参数模板。
- **注意**: `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. 参数与日志接口 (Custom Actions)
#### 3.8.1. 获取状态流转记录列表 #### 3.8.1. 获取状态流转记录列表
@@ -737,7 +768,7 @@ token = response.json()['access']
# 创建状态 # 创建状态
headers = {'Authorization': f'Bearer {token}'} headers = {'Authorization': f'Bearer {token}'}
response = requests.post( response = requests.post(
'http://localhost:8000/api/v1/states/', 'http://localhost:8000/api/v1/stateflow/states/',
json={ json={
'name': '待审核', 'name': '待审核',
'description': '等待审核' 'description': '等待审核'
@@ -745,26 +776,50 @@ response = requests.post(
headers=headers headers=headers
) )
state = response.json() 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 (fetch)
```javascript ```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: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
}); });
const data = await response.json(); const data = await response.json();
console.log(data.results); 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 ### curl
```bash ```bash
# 创建流程 # 创建流程
curl -X POST "http://localhost:8000/api/v1/processes/" \ curl -X POST "http://localhost:8000/api/v1/stateflow/processes/" \
-H "Authorization: Bearer <token>" \ -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -775,4 +830,8 @@ curl -X POST "http://localhost:8000/api/v1/processes/" \
{"state_id": 2, "order": 1} {"state_id": 2, "order": 1}
] ]
}' }'
# 获取业务对象的流程节点
curl -X GET "http://localhost:8000/api/v1/stateflow/business-objects/1/process-nodes/" \
-H "Authorization: Bearer <token>"
``` ```

View File

@@ -476,3 +476,34 @@ def add_parameters_to_state_log(state_log: 'models.StateFlowRecord', remark: str
StateLogParameterRecord: 创建的参数记录 StateLogParameterRecord: 创建的参数记录
""" """
return create_parameter_record(state_log, remark, **parameters) 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
]

View File

@@ -652,3 +652,39 @@ class BusinessObjectAPITestCase(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success']) self.assertFalse(response.data['success'])
self.assertIn('流程没有任何节点', response.data['message']) 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)

View File

@@ -288,3 +288,33 @@ class StateFlowServicesTestCase(TestCase):
self.state3.parameters.add(required_param) self.state3.parameters.add(required_param)
params = services.get_state_parameters(self.state3, required_only=True) params = services.get_state_parameters(self.state3, required_only=True)
self.assertEqual(len(params), 1) 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)