1
0
forked from erp-dev/erp

feat: stateflow v2 (params required)

This commit is contained in:
2025-11-15 19:39:15 +08:00
parent 9ad404a365
commit 9c38e2ac09
24 changed files with 3670 additions and 547 deletions

View File

@@ -257,11 +257,13 @@ class PrintingJobViewSet(viewsets.ModelViewSet):
create: 创建款式明细 create: 创建款式明细
update: 更新款式明细 update: 更新款式明细
partial_update: 部分更新款式明细 partial_update: 部分更新款式明细
advance_to_next_state: 推进到下一个状态
step_back_one_state: 回退一步
completed_states: 查询已完成的流程列表 completed_states: 查询已完成的流程列表
timeline: 获取流程时间线 timeline: 获取流程时间线
注意:状态流转功能已迁移至 stateflow 模块的业务对象接口
- 推进到下一步: POST /api/v1/stateflow/business-objects/{business_object_id}/advance/
- 回退一步: POST /api/v1/stateflow/business-objects/{business_object_id}/step_back/
查询参数: 查询参数:
- printing_order: 印染订单ID - printing_order: 印染订单ID
- product: 产品ID - product: 产品ID
@@ -306,76 +308,6 @@ class PrintingJobViewSet(viewsets.ModelViewSet):
status=status.HTTP_405_METHOD_NOT_ALLOWED status=status.HTTP_405_METHOD_NOT_ALLOWED
) )
@action(detail=True, methods=['post'], url_path='advance-to-next-state')
def advance_to_next_state(self, request, pk=None):
"""
推进到下一个状态
将印染任务推进到下一个流程状态
"""
job = self.get_object()
if not job.business_object:
return Response(
{'detail': '该任务没有关联的流程实例'},
status=status.HTTP_400_BAD_REQUEST
)
from stateflow import services as stateflow_services
success, message = stateflow_services.advance_to_next_state(
job.business_object, request.user
)
if not success:
return Response(
{'detail': message},
status=status.HTTP_400_BAD_REQUEST
)
# 重新获取 job 以刷新状态
job.refresh_from_db()
serializer = PrintingJobDetailSerializer(job)
return Response({
'detail': message,
'data': serializer.data
})
@action(detail=True, methods=['post'], url_path='step-back-one-state')
def step_back_one_state(self, request, pk=None):
"""
回退一步
将印染任务回退到上一个流程状态
"""
job = self.get_object()
if not job.business_object:
return Response(
{'detail': '该任务没有关联的流程实例'},
status=status.HTTP_400_BAD_REQUEST
)
from stateflow import services as stateflow_services
success, message = stateflow_services.step_back_one_state(
job.business_object, request.user
)
if not success:
return Response(
{'detail': message},
status=status.HTTP_400_BAD_REQUEST
)
# 重新获取 job 以刷新状态
job.refresh_from_db()
serializer = PrintingJobDetailSerializer(job)
return Response({
'detail': message,
'data': serializer.data
})
@action(detail=True, methods=['get'], url_path='completed-states') @action(detail=True, methods=['get'], url_path='completed-states')
def completed_states(self, request, pk=None): def completed_states(self, request, pk=None):
""" """
@@ -520,11 +452,13 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
partial_update: 部分更新开版订单 partial_update: 部分更新开版订单
invalidate: 作废开版订单 invalidate: 作废开版订单
activate: 恢复开版订单 activate: 恢复开版订单
advance_to_next_state: 推进到下一个状态
step_back_one_state: 回退一步
completed_states: 查询已完成的流程列表 completed_states: 查询已完成的流程列表
timeline: 获取流程时间线 timeline: 获取流程时间线
注意:状态流转功能已迁移至 stateflow 模块的业务对象接口
- 推进到下一步: POST /api/v1/stateflow/business-objects/{business_object_id}/advance/
- 回退一步: POST /api/v1/stateflow/business-objects/{business_object_id}/step_back/
查询参数: 查询参数:
- customer: 客户ID - customer: 客户ID
- customer_name: 客户名称(模糊查询) - customer_name: 客户名称(模糊查询)
@@ -653,78 +587,6 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
'data': serializer.data 'data': serializer.data
}) })
@action(detail=True, methods=['post'], url_path='advance-to-next-state')
def advance_to_next_state(self, request, pk=None):
"""
推进到下一个状态
将开版订单推进到下一个流程状态
"""
plate_order = self.get_object()
if not plate_order.business_object:
return Response(
{'detail': '该开版订单没有关联的流程实例'},
status=status.HTTP_400_BAD_REQUEST
)
from stateflow import services as stateflow_services
success, message = stateflow_services.advance_to_next_state(
plate_order.business_object, request.user
)
if not success:
return Response(
{'detail': message},
status=status.HTTP_400_BAD_REQUEST
)
# 重新获取 plate_order 以刷新状态
plate_order.refresh_from_db()
from .serializers import PlateOrderDetailSerializer
serializer = PlateOrderDetailSerializer(plate_order, context={'request': request})
return Response({
'detail': message,
'data': serializer.data
})
@action(detail=True, methods=['post'], url_path='step-back-one-state')
def step_back_one_state(self, request, pk=None):
"""
回退一步
将开版订单回退到上一个流程状态
"""
plate_order = self.get_object()
if not plate_order.business_object:
return Response(
{'detail': '该开版订单没有关联的流程实例'},
status=status.HTTP_400_BAD_REQUEST
)
from stateflow import services as stateflow_services
success, message = stateflow_services.step_back_one_state(
plate_order.business_object, request.user
)
if not success:
return Response(
{'detail': message},
status=status.HTTP_400_BAD_REQUEST
)
# 重新获取 plate_order 以刷新状态
plate_order.refresh_from_db()
from .serializers import PlateOrderDetailSerializer
serializer = PlateOrderDetailSerializer(plate_order, context={'request': request})
return Response({
'detail': message,
'data': serializer.data
})
@action(detail=True, methods=['get'], url_path='completed-states') @action(detail=True, methods=['get'], url_path='completed-states')
def completed_states(self, request, pk=None): def completed_states(self, request, pk=None):
""" """

View File

@@ -117,16 +117,32 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def advance(self, request, pk=None): def advance(self, request, pk=None):
"""推进到下一个状态""" """
推进到下一个状态
请求体:
{
"parameters": {
"temperature": "25.5",
"humidity": "60%",
"operator": "张三"
}
}
参数可选,但如果下一个状态有必填参数,则必须提供
"""
business_object = self.get_object() business_object = self.get_object()
user = request.user user = request.user
parameters = request.data.get('parameters', {})
success, message = services.advance_to_next_state(business_object, user) success, message, state_log = services.advance_to_next_state(business_object, user, **parameters)
if success: if success:
from stateflow.serializers import StateFlowRecordWithParametersSerializer
return Response({ return Response({
'success': True, 'success': True,
'message': message, 'message': message,
'state_log': StateFlowRecordWithParametersSerializer(state_log).data if state_log else None,
'business_object': BusinessObjectDetailSerializer(business_object).data 'business_object': BusinessObjectDetailSerializer(business_object).data
}) })
else: else:
@@ -187,3 +203,174 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
] ]
return Response(timeline_data) return Response(timeline_data)
@action(detail=True, methods=['get'])
def next_pending_state(self, request, pk=None):
"""
获取最近一个待执行节点
查询参数:
- include_parameters: 是否包含参数列表,默认 true (可选值: true, false)
"""
business_object = self.get_object()
include_parameters = request.query_params.get('include_parameters', 'true').lower() == 'true'
next_state = services.get_next_pending_state(business_object, include_parameters=include_parameters)
if next_state is None:
return Response({
'next_state': None,
'message': '没有待执行节点(未开始或已完成)'
})
from stateflow.serializers import StateParameterSerializer
result = {
'state': StateListSerializer(next_state['state']).data,
'order': next_state['order'],
}
if 'parameters' in next_state:
result['parameters'] = StateParameterSerializer(next_state['parameters'], many=True).data
return Response(result)
@action(detail=True, methods=['get'])
def pending_states(self, request, pk=None):
"""获取所有待执行节点列表(不包含参数)"""
business_object = self.get_object()
pending_states = services.get_all_pending_states(business_object)
result = [
{
'state': StateListSerializer(item['state']).data,
'order': item['order'],
}
for item in pending_states
]
return Response({
'count': len(result),
'pending_states': result
})
@action(detail=True, methods=['get'], url_path='current_state_parameters')
def current_state_parameters(self, request, pk=None):
"""
获取当前状态(最后完成的状态)的参数列表
查询参数:
- required_only: 是否只返回必填参数,默认 false (可选值: true, false)
注意current_state 表示最后完成的状态,如果未开始则返回空
"""
business_object = self.get_object()
required_only = request.query_params.get('required_only', 'false').lower() == 'true'
current_state = services.get_business_object_current_state(business_object)
if current_state is None:
return Response({
'parameters': [],
'message': '尚未完成任何状态'
})
from stateflow.serializers import StateParameterSerializer
parameters = services.get_state_parameters(current_state, required_only=required_only)
return Response({
'state': StateListSerializer(current_state).data,
'parameters': StateParameterSerializer(parameters, many=True).data,
'count': len(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):
"""
为状态流转记录补充参数
POST /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/add-parameters/
请求体:
{
"parameters": {
"additional_note": "发现轻微瑕疵",
"temperature": "26.0"
},
"remark": "质检员补充"
}
"""
business_object = self.get_object()
try:
state_log = business_object.state_logs.get(id=log_id)
except models.StateFlowRecord.DoesNotExist:
return Response({
'success': False,
'message': '状态流转记录不存在'
}, status=status.HTTP_404_NOT_FOUND)
parameters = request.data.get('parameters', {})
remark = request.data.get('remark', '')
if not parameters:
return Response({
'success': False,
'message': '参数不能为空'
}, status=status.HTTP_400_BAD_REQUEST)
from stateflow.serializers import StateLogParameterRecordSerializer
record = services.add_parameters_to_state_log(state_log, remark=remark, **parameters)
return Response({
'success': True,
'parameter_record': StateLogParameterRecordSerializer(record).data
})
@action(detail=True, methods=['get'], url_path='state-logs/(?P<log_id>[^/.]+)/parameters')
def get_log_parameters(self, request, pk=None, log_id=None):
"""
获取状态流转记录的所有参数记录
GET /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/
GET /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/?key=temperature
GET /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/?include_cancelled=true
查询参数:
- key: 指定参数键,返回该参数的历史(可选)
- include_cancelled: 是否包含已撤销状态的参数,默认 false可选
"""
business_object = self.get_object()
try:
state_log = business_object.state_logs.get(id=log_id)
except models.StateFlowRecord.DoesNotExist:
return Response({
'success': False,
'message': '状态流转记录不存在'
}, status=status.HTTP_404_NOT_FOUND)
key = request.query_params.get('key')
include_cancelled = request.query_params.get('include_cancelled', 'false').lower() == 'true'
if key:
# 返回指定参数的历史
history = state_log.get_parameter_history(key, include_cancelled=include_cancelled)
return Response({
'state_log_id': state_log.id,
'key': key,
'is_cancelled': state_log.is_cancelled,
'history': history
})
else:
# 返回所有参数记录
from stateflow.serializers import StateLogParameterRecordSerializer
records = state_log.parameter_records.all()
return Response({
'state_log_id': state_log.id,
'is_cancelled': state_log.is_cancelled,
'count': records.count(),
'summary': state_log.get_all_parameters_summary(include_cancelled=include_cancelled),
'records': StateLogParameterRecordSerializer(records, many=True).data if include_cancelled or not state_log.is_cancelled else []
})

View File

@@ -2,13 +2,16 @@
State API ViewSet State API ViewSet
""" """
from rest_framework import viewsets, filters from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from stateflow import models from stateflow import models, services
from stateflow.serializers import ( from stateflow.serializers import (
StateListSerializer, StateListSerializer,
StateDetailSerializer, StateDetailSerializer,
StateCreateUpdateSerializer, StateCreateUpdateSerializer,
StateParameterSerializer,
) )
@@ -51,3 +54,23 @@ class StateViewSet(viewsets.ModelViewSet):
if self.action == 'retrieve': if self.action == 'retrieve':
queryset = queryset.prefetch_related('parameters') queryset = queryset.prefetch_related('parameters')
return queryset return queryset
@action(detail=True, methods=['get'])
def parameters(self, request, pk=None):
"""
获取状态的参数列表
查询参数:
- required_only: 是否只返回必填参数,默认 false (可选值: true, false)
"""
state = self.get_object()
required_only = request.query_params.get('required_only', 'false').lower() == 'true'
parameters = services.get_state_parameters(state, required_only=required_only)
return Response({
'state_id': state.id,
'state_name': state.name,
'parameters': StateParameterSerializer(parameters, many=True).data,
'count': len(parameters)
})

View File

@@ -132,7 +132,7 @@ class PlateOrderAdmin(admin.ModelAdmin):
messages.append(f"{obj.id}: 没有关联的流程实例,无法推进") messages.append(f"{obj.id}: 没有关联的流程实例,无法推进")
continue continue
success, message = services.advance_to_next_state(obj.business_object, request.user) success, message, _ = services.advance_to_next_state(obj.business_object, request.user)
if success: if success:
success_count += 1 success_count += 1
messages.append(f"{obj.id}: {message}") messages.append(f"{obj.id}: {message}")

View File

@@ -54,7 +54,6 @@ class PlateOrderModelTestCase(TestCase):
def test_create_plate_order_basic(self): def test_create_plate_order_basic(self):
"""测试创建基本的开版订单""" """测试创建基本的开版订单"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-1',
design_code='DES001', design_code='DES001',
customer=self.customer, customer=self.customer,
plate_type='首版', plate_type='首版',
@@ -65,14 +64,14 @@ class PlateOrderModelTestCase(TestCase):
) )
self.assertIsNotNone(plate_order.id) self.assertIsNotNone(plate_order.id)
self.assertEqual(plate_order.plate_code, '20251113-1') self.assertEqual(plate_order.design_code, 'DES001')
self.assertEqual(plate_order.customer, self.customer) self.assertEqual(plate_order.customer, self.customer)
self.assertEqual(plate_order.style_name, '测试款式') self.assertEqual(plate_order.style_name, '测试款式')
def test_plate_order_with_employees(self): def test_plate_order_with_employees(self):
"""测试带销售员和跟单员的开版订单""" """测试带销售员和跟单员的开版订单"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-2', design_code='DES002',
customer=self.customer, customer=self.customer,
salesperson=self.salesperson, salesperson=self.salesperson,
merchandiser=self.merchandiser, merchandiser=self.merchandiser,
@@ -86,7 +85,7 @@ class PlateOrderModelTestCase(TestCase):
def test_plate_order_with_ratings(self): def test_plate_order_with_ratings(self):
"""测试带质量评级的开版订单""" """测试带质量评级的开版订单"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-3', design_code='DES003',
customer=self.customer, customer=self.customer,
drawing_rating='A', drawing_rating='A',
color_matching_rating='B', color_matching_rating='B',
@@ -108,7 +107,7 @@ class PlateOrderModelTestCase(TestCase):
required_date = (now + timezone.timedelta(days=10)).date() required_date = (now + timezone.timedelta(days=10)).date()
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-4', design_code='DES004',
customer=self.customer, customer=self.customer,
plate_date=now, plate_date=now,
completion_date=completion_date, completion_date=completion_date,
@@ -122,7 +121,7 @@ class PlateOrderModelTestCase(TestCase):
def test_plate_order_with_sample_info(self): def test_plate_order_with_sample_info(self):
"""测试带样品信息的开版订单""" """测试带样品信息的开版订单"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-5', design_code='DES005',
customer=self.customer, customer=self.customer,
sample_meter='米样1', sample_meter='米样1',
required_sample_meters=Decimal('100.50'), required_sample_meters=Decimal('100.50'),
@@ -134,7 +133,7 @@ class PlateOrderModelTestCase(TestCase):
def test_plate_order_status_without_business_object(self): def test_plate_order_status_without_business_object(self):
"""测试没有 business_object 时的状态""" """测试没有 business_object 时的状态"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-6', design_code='DES006',
customer=self.customer, customer=self.customer,
) )
@@ -161,7 +160,7 @@ class PlateOrderModelTestCase(TestCase):
# 创建开版订单 # 创建开版订单
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-7', design_code='DES007',
customer=self.customer, customer=self.customer,
business_object=business_object, business_object=business_object,
) )
@@ -175,7 +174,7 @@ class PlateOrderModelTestCase(TestCase):
# 刷新并检查状态 # 刷新并检查状态
plate_order.refresh_from_db() plate_order.refresh_from_db()
self.assertEqual(plate_order.status, state2.name) # 应该在第二个状态 self.assertEqual(plate_order.status, state1.name) # current_state 是最后完成的状态state1
self.assertFalse(plate_order.is_completed) self.assertFalse(plate_order.is_completed)
self.assertTrue(plate_order.has_started) self.assertTrue(plate_order.has_started)
self.assertGreater(plate_order.progress_percentage, 0) self.assertGreater(plate_order.progress_percentage, 0)
@@ -183,16 +182,16 @@ class PlateOrderModelTestCase(TestCase):
def test_plate_order_str(self): def test_plate_order_str(self):
"""测试字符串表示""" """测试字符串表示"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-8', design_code='DES008',
customer=self.customer, customer=self.customer,
) )
self.assertEqual(str(plate_order), '20251113-8') self.assertEqual(str(plate_order), f'PlateOrder-{plate_order.id}')
def test_plate_order_with_approval_and_order_status(self): def test_plate_order_with_approval_and_order_status(self):
"""测试带审批和订单状态的开版订单""" """测试带审批和订单状态的开版订单"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-9', design_code='DES009',
customer=self.customer, customer=self.customer,
approval_result='通过', approval_result='通过',
is_ordered=True, is_ordered=True,
@@ -203,24 +202,19 @@ class PlateOrderModelTestCase(TestCase):
self.assertTrue(plate_order.is_ordered) self.assertTrue(plate_order.is_ordered)
self.assertEqual(plate_order.customer_feedback, '颜色需要调整') self.assertEqual(plate_order.customer_feedback, '颜色需要调整')
def test_plate_order_unique_plate_code(self): def test_plate_order_with_invalid_flag(self):
"""测试版单编号的唯一性""" """测试作废标记"""
printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-10', design_code='DES010',
customer=self.customer, customer=self.customer,
is_invalid=True,
) )
# 尝试创建相同编号的订单应该失败 self.assertTrue(plate_order.is_invalid)
with self.assertRaises(Exception):
printing_models.PlateOrder.objects.create(
plate_code='20251113-10',
customer=self.customer,
)
def test_plate_order_nullable_fields(self): def test_plate_order_nullable_fields(self):
"""测试可空字段""" """测试可空字段"""
plate_order = printing_models.PlateOrder.objects.create( plate_order = printing_models.PlateOrder.objects.create(
plate_code='20251113-11',
customer=self.customer, customer=self.customer,
) )

View File

@@ -0,0 +1,583 @@
# Stateflow 业务对象 API 文档
## 概述
业务对象BusinessObject是流程的实例支持状态流转、参数管理和进度控制。
**基础路径**: `/api/v1/stateflow/business-objects/`
---
## 1. 查询下一步节点(可选带参数)
### 获取下一个待执行节点
**接口**: `GET /api/v1/stateflow/business-objects/{id}/next_pending_state/`
**描述**: 获取业务对象的下一个待执行状态节点,默认包含该状态的参数定义列表。
**查询参数**:
- `include_parameters` (可选): 是否包含参数列表,默认 `true`
- `true`: 返回参数定义列表
- `false`: 只返回状态信息
**成功响应** (200):
```json
{
"state": {
"id": 5,
"name": "质量检验",
"description": "对产品进行质量检查"
},
"order": 3,
"parameters": [
{
"id": 1,
"name": "temperature",
"display_name": "温度",
"is_required": true,
"is_image_path": false
},
{
"id": 2,
"name": "humidity",
"display_name": "湿度",
"is_required": false,
"is_image_path": false
}
]
}
```
**无待执行节点** (200):
```json
{
"next_state": null,
"message": "没有待执行节点(未开始或已完成)"
}
```
**示例**:
```bash
# 获取下一个节点及参数
GET /api/v1/stateflow/business-objects/123/next_pending_state/
# 只获取下一个节点,不要参数
GET /api/v1/stateflow/business-objects/123/next_pending_state/?include_parameters=false
```
---
## 2. 查询已存在的参数(可选仅包括必填)
### 2.1 获取当前状态的参数定义
**接口**: `GET /api/v1/stateflow/business-objects/{id}/current_state_parameters/`
**描述**: 获取当前状态(最后完成的状态)的参数定义列表。
**查询参数**:
- `required_only` (可选): 是否只返回必填参数,默认 `false`
- `true`: 只返回必填参数
- `false`: 返回所有参数
**成功响应** (200):
```json
{
"state": {
"id": 4,
"name": "生产加工",
"description": "进行生产加工"
},
"parameters": [
{
"id": 3,
"name": "machine_number",
"display_name": "机器编号",
"is_required": true,
"is_image_path": false
},
{
"id": 4,
"name": "operator",
"display_name": "操作员",
"is_required": true,
"is_image_path": false
}
],
"count": 2
}
```
**未完成任何状态** (200):
```json
{
"parameters": [],
"message": "尚未完成任何状态"
}
```
**示例**:
```bash
# 获取当前状态的所有参数定义
GET /api/v1/stateflow/business-objects/123/current_state_parameters/
# 只获取必填参数
GET /api/v1/stateflow/business-objects/123/current_state_parameters/?required_only=true
```
### 2.2 获取状态记录的实际参数值
**接口**: `GET /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/`
**描述**: 获取指定状态流转记录的实际参数值(用户填写的数据)。
**查询参数**:
- `key` (可选): 指定参数键,返回该参数的历史记录
- `include_cancelled` (可选): 是否包含已撤销状态的参数,默认 `false`
**成功响应 - 所有参数** (200):
```json
{
"state_log_id": 456,
"is_cancelled": false,
"count": 2,
"summary": {
"temperature": "25.5",
"humidity": "60%",
"operator": "张三"
},
"records": [
{
"id": 1,
"state_log": 456,
"parameters": {
"temperature": "25.5",
"humidity": "60%"
},
"remark": "",
"created_at": "2025-11-15T10:30:00Z",
"created_by": 1
},
{
"id": 2,
"state_log": 456,
"parameters": {
"operator": "张三"
},
"remark": "补充操作员信息",
"created_at": "2025-11-15T11:00:00Z",
"created_by": 2
}
]
}
```
**成功响应 - 指定参数历史** (200):
```json
{
"state_log_id": 456,
"key": "temperature",
"is_cancelled": false,
"history": [
{
"value": "25.5",
"created_at": "2025-11-15T10:30:00Z",
"created_by": 1,
"remark": ""
}
]
}
```
**状态记录不存在** (404):
```json
{
"success": false,
"message": "状态流转记录不存在"
}
```
**示例**:
```bash
# 获取状态记录的所有参数
GET /api/v1/stateflow/business-objects/123/state-logs/456/parameters/
# 获取特定参数的历史
GET /api/v1/stateflow/business-objects/123/state-logs/456/parameters/?key=temperature
# 包含已撤销状态的参数
GET /api/v1/stateflow/business-objects/123/state-logs/456/parameters/?include_cancelled=true
```
---
## 3. 推进到下一步(校验必填参数)
**接口**: `POST /api/v1/stateflow/business-objects/{id}/advance/`
**描述**: 推进业务对象到下一个状态,自动校验必填参数。
**请求体**:
```json
{
"parameters": {
"temperature": "25.5",
"humidity": "60%",
"operator": "张三"
}
}
```
**参数说明**:
- `parameters` (可选): 参数字典
- 如果下一个状态有必填参数,则必须提供所有必填参数
- 可以包含可选参数
- 参数值以字符串形式存储
**成功响应** (200):
```json
{
"success": true,
"message": "已推进到状态: 质量检验",
"state_log": {
"id": 456,
"state": {
"id": 5,
"name": "质量检验"
},
"completed_at": "2025-11-15T10:30:00Z",
"completed_by": {
"id": 1,
"username": "zhangsan"
},
"is_cancelled": false,
"parameters_summary": {
"temperature": "25.5",
"humidity": "60%"
}
},
"business_object": {
"id": 123,
"name": "订单-001",
"process": {...},
"overall_status": "in_progress",
"current_state": {...}
}
}
```
**校验失败 - 缺少必填参数** (400):
```json
{
"success": false,
"message": "缺少必填参数: temperature, operator"
}
```
**其他失败情况** (400):
```json
{
"success": false,
"message": "已经完成所有状态"
}
```
**示例**:
```bash
# 推进到下一状态(带参数)
curl -X POST /api/v1/stateflow/business-objects/123/advance/ \
-H "Content-Type: application/json" \
-d '{"parameters": {"temperature": "25.5", "humidity": "60%"}}'
# 推进到下一状态(无参数)
curl -X POST /api/v1/stateflow/business-objects/123/advance/ \
-H "Content-Type: application/json" \
-d '{}'
```
---
## 4. 回退
**接口**: `POST /api/v1/stateflow/business-objects/{id}/step_back/`
**描述**: 回退一步,撤销最后一次完成的状态(标记为已撤销,不删除记录)。
**请求体**: 无需请求体
**成功响应** (200):
```json
{
"success": true,
"message": "已回退到状态: 生产加工",
"business_object": {
"id": 123,
"name": "订单-001",
"process": {...},
"overall_status": "in_progress",
"current_state": {...}
}
}
```
**失败响应** (400):
```json
{
"success": false,
"message": "没有可回退的状态"
}
```
**示例**:
```bash
curl -X POST /api/v1/stateflow/business-objects/123/step_back/
```
---
## 5. 针对已完成的状态节点添加新的参数集
**接口**: `POST /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/add-parameters/`
**描述**: 为已完成的状态流转记录补充参数(追加新的参数记录)。
**请求体**:
```json
{
"parameters": {
"additional_note": "发现轻微瑕疵",
"inspector": "李四"
},
"remark": "质检员补充信息"
}
```
**参数说明**:
- `parameters` (必填): 参数字典,不能为空
- `remark` (可选): 备注说明
**成功响应** (200):
```json
{
"success": true,
"parameter_record": {
"id": 789,
"state_log": 456,
"parameters": {
"additional_note": "发现轻微瑕疵",
"inspector": "李四"
},
"remark": "质检员补充信息",
"created_at": "2025-11-15T14:30:00Z",
"created_by": 2
}
}
```
**失败响应 - 状态记录不存在** (404):
```json
{
"success": false,
"message": "状态流转记录不存在"
}
```
**失败响应 - 参数为空** (400):
```json
{
"success": false,
"message": "参数不能为空"
}
```
**示例**:
```bash
curl -X POST /api/v1/stateflow/business-objects/123/state-logs/456/add-parameters/ \
-H "Content-Type: application/json" \
-d '{
"parameters": {
"additional_note": "发现轻微瑕疵",
"inspector": "李四"
},
"remark": "质检员补充信息"
}'
```
---
## 6. 其他相关接口
### 6.1 获取状态时间线
**接口**: `GET /api/v1/stateflow/business-objects/{id}/timeline/`
**描述**: 获取业务对象的完整状态时间线。
**成功响应** (200):
```json
[
{
"state": {
"id": 1,
"name": "接单"
},
"status": "completed",
"order": 1,
"completed_at": "2025-11-15T09:00:00Z",
"completed_by": "zhangsan",
"cancelled_at": null,
"is_cancelled": false
},
{
"state": {
"id": 2,
"name": "生产加工"
},
"status": "completed",
"order": 2,
"completed_at": "2025-11-15T10:00:00Z",
"completed_by": "lisi",
"cancelled_at": null,
"is_cancelled": false
},
{
"state": {
"id": 3,
"name": "质量检验"
},
"status": "pending",
"order": 3,
"completed_at": null,
"completed_by": null,
"cancelled_at": null,
"is_cancelled": false
}
]
```
### 6.2 获取所有待执行节点
**接口**: `GET /api/v1/stateflow/business-objects/{id}/pending_states/`
**描述**: 获取所有待执行的状态节点列表(不包含参数)。
**成功响应** (200):
```json
{
"count": 2,
"pending_states": [
{
"state": {
"id": 3,
"name": "质量检验"
},
"order": 3
},
{
"state": {
"id": 4,
"name": "打包发货"
},
"order": 4
}
]
}
```
### 6.3 重置进度
**接口**: `POST /api/v1/stateflow/business-objects/{id}/reset/`
**描述**: 重置业务对象的进度(删除所有状态记录)。
**成功响应** (200):
```json
{
"success": true,
"message": "进度已重置",
"business_object": {...}
}
```
---
## 完整性检查清单
根据您的需求,所有接口都已齐全:
-**查询下一步节点(可选带参数)**: `GET next_pending_state/` (支持 `include_parameters` 参数)
-**查询已存在的参数(可选仅包括必填)**:
- `GET current_state_parameters/` (支持 `required_only` 参数) - 参数定义
- `GET state-logs/{log_id}/parameters/` - 实际参数值
-**回退**: `POST step_back/`
-**推进到下一步(校验必填参数)**: `POST advance/` (自动校验必填参数)
-**针对已完成的状态节点添加新的参数集**: `POST state-logs/{log_id}/add-parameters/`
---
## 典型使用流程
### 场景 1: 推进到下一状态
```bash
# 1. 查询下一个待执行节点及参数要求
GET /api/v1/stateflow/business-objects/123/next_pending_state/
# 响应显示需要 temperature (必填) 和 humidity (可选)
# 2. 推进到下一状态(提供必填参数)
POST /api/v1/stateflow/business-objects/123/advance/
{
"parameters": {
"temperature": "25.5",
"humidity": "60%"
}
}
```
### 场景 2: 补充参数
```bash
# 1. 从时间线获取已完成状态的 state_log_id
GET /api/v1/stateflow/business-objects/123/timeline/
# 2. 为指定状态记录补充参数
POST /api/v1/stateflow/business-objects/123/state-logs/456/add-parameters/
{
"parameters": {
"inspector": "李四",
"note": "补充检验员信息"
},
"remark": "质检后补充"
}
```
### 场景 3: 查询参数
```bash
# 1. 查询当前状态的必填参数定义
GET /api/v1/stateflow/business-objects/123/current_state_parameters/?required_only=true
# 2. 查询某个状态记录的实际参数值
GET /api/v1/stateflow/business-objects/123/state-logs/456/parameters/
# 3. 查询某个参数的修改历史
GET /api/v1/stateflow/business-objects/123/state-logs/456/parameters/?key=temperature
```
---
## 重要说明
1. **参数存储**: 所有参数值以字符串形式存储在 JSONField 中,支持灵活的数据结构
2. **参数校验**: 仅校验必填参数是否提供,不校验参数值的格式或类型
3. **撤销状态**: 回退操作不删除记录,而是标记为已撤销(`is_cancelled=True`
4. **参数可见性**: 默认情况下,已撤销状态的参数不会返回,需要显式传递 `include_cancelled=true`
5. **参数追加**: 可以多次为同一个状态记录添加参数,所有记录都会保留
6. **参数历史**: 支持查询单个参数的修改历史,按时间顺序返回
---
**文档版本**: 2025-11-15
**测试覆盖**: 55 个测试用例全部通过

View File

@@ -0,0 +1,402 @@
# Stateflow 模块更新日志 - 2025年11月15日
## 概述
本次更新对 stateflow 模块进行了重大重构主要涉及数据模型关系调整、新增字段、服务层功能扩展、API 接口增强以及测试优化。
---
## 一、数据模型变更
### 1.1 State 与 StateParameter 关系重构
**Migration: 0017_state_parameter_many_to_many**
- **变更内容**:将 State 与 StateParameter 的关系从 **一对多** 改为 **多对多**
- **原设计**StateParameter 通过 ForeignKey 关联到 State一个参数只能属于一个状态
- **新设计**State 通过 ManyToManyField 关联 StateParameter多个状态可以共享同一个参数
- **数据迁移**:清空了旧的测试数据
- **影响范围**
- 模型定义:`State.parameters = models.ManyToManyField(StateParameter)`
- API 接口:需要使用 `parameter_ids` 进行参数关联
- 序列化器:支持参数列表的读写操作
### 1.2 移除 StateParameter.name 字段
**Migration: 0018_remove_stateparameter_name**
- **变更原因**`name` 字段与 `key` 字段语义重复
- **保留字段**:使用 `key` 作为参数的唯一标识符
- **影响**:简化了参数模型,避免了字段冗余
### 1.3 新增 StateParameter.is_required 字段
**Migration: 0019_stateparameter_is_required**
- **字段类型**`BooleanField(default=False)`
- **用途**:标记参数是否为必填项
- **应用场景**
- 前端表单验证
- API 可以通过 `required_only=true` 查询参数只获取必填参数
- 业务逻辑中强制验证必填参数的提供
### 1.4 新增 StateParameter.is_image_path 字段
**Migration: 0020_stateparameter_is_image_path**
- **字段类型**`BooleanField(default=False)`
- **用途**:标记 `value` 字段是否存储图片 URL 路径
- **应用场景**
- 前端根据此字段决定是否以图片形式渲染
- 支持混合参数类型(文本、图片、附件等)
- 便于后续扩展不同类型的参数
---
## 二、current_state 语义修正(重要变更)
### 2.1 语义变更
**原语义**`current_state` 表示"下一个待执行的节点"(未完成的节点)
**新语义**`current_state` 表示"最后完成的状态"(已完成的节点)
### 2.2 影响的函数
#### stateflow/services.py
1. **get_business_object_current_state()**
- 原逻辑:返回第一个未完成的节点
- 新逻辑:返回最后一个已完成的节点,未开始时返回 None
2. **advance_to_next_state()**
- 更新:基于新的 current_state 逻辑计算下一个待完成节点
3. **get_next_pending_state()** (新增函数)
- 用途:获取下一个待执行节点(替代了原 current_state 的部分功能)
4. **get_business_object_state_status()**
- 移除:删除了 'in_progress' 状态
- 保留:只有 'not_started' 和 'completed' 两种状态
5. **get_overall_status()**
- 更新:基于新的 current_state 逻辑判断整体状态
6. **can_advance_to_next_state()**
- 更新:判断逻辑基于新的 current_state 语义
7. **get_business_object_state_timeline()**
- 更新:时间线生成逻辑适配新语义
#### stateflow/admin.py
- **BusinessObjectAdmin.current_state_display()**
- 更新:显示最后完成的状态,未开始时显示"未开始"
### 2.3 测试更新
- 更新了 34 个既有测试以适配新语义
- 新增 9 个测试用例
- 所有 43 个测试全部通过
---
## 三、新增服务层功能
### 3.1 get_next_pending_state()
**位置**`stateflow/services.py`
```python
def get_next_pending_state(business_object, include_parameters=True):
"""
获取业务对象的下一个待执行节点
参数:
business_object: 业务对象实例
include_parameters: 是否包含节点参数(默认 True
返回:
dict 或 None: 包含 state, order, parameters可选
"""
```
**功能**
- 返回流程中下一个未完成的节点
- 可选择是否包含该节点的参数列表
- 流程完成时返回 None
**测试**`test_get_next_pending_state()` - 已通过
### 3.2 get_all_pending_states()
**位置**`stateflow/services.py`
```python
def get_all_pending_states(business_object):
"""
获取业务对象的所有待执行节点列表
参数:
business_object: 业务对象实例
返回:
list: 包含所有待执行节点的列表
"""
```
**功能**
- 返回所有尚未完成的节点列表(按顺序)
- 不包含参数信息(减少数据量)
- 适用于显示整体进度
**测试**`test_get_all_pending_states()` - 已通过
### 3.3 get_state_parameters()
**位置**`stateflow/services.py`
```python
def get_state_parameters(state, required_only=False):
"""
获取状态的参数列表
参数:
state: State 实例
required_only: 是否只返回必填参数(默认 False
返回:
QuerySet: StateParameter 查询集
"""
```
**功能**
- 获取指定状态的所有参数
- 支持筛选只返回必填参数
- 基于多对多关系查询
**测试**`test_get_state_parameters()` - 已通过
---
## 四、新增 API 接口
### 4.1 获取下一个待执行节点
**端点**`GET /api/v1/stateflow/business-objects/{id}/next_pending_state/`
**查询参数**
- `include_parameters`: boolean默认 true- 是否包含参数
**响应示例**
```json
{
"business_object_id": 1,
"next_state": {
"state": {
"id": 1,
"name": "状态1",
"description": "第一个状态"
},
"order": 0,
"parameters": [
{
"id": 1,
"key": "param1",
"value": "value1",
"is_required": true,
"is_image_path": false
}
]
}
}
```
**测试**`test_next_pending_state_api()` - 已通过
### 4.2 获取所有待执行节点
**端点**`GET /api/v1/stateflow/business-objects/{id}/pending_states/`
**响应示例**
```json
{
"business_object_id": 1,
"count": 3,
"pending_states": [
{
"state": {"id": 1, "name": "状态1"},
"order": 0
},
{
"state": {"id": 2, "name": "状态2"},
"order": 1
}
]
}
```
**测试**`test_pending_states_api()` - 已通过
### 4.3 获取当前状态的参数
**端点**`GET /api/v1/stateflow/business-objects/{id}/current_state_parameters/`
**查询参数**
- `required_only`: boolean默认 false- 是否只返回必填参数
**响应示例**
```json
{
"business_object_id": 1,
"state": {
"id": 1,
"name": "状态1"
},
"count": 2,
"parameters": [
{
"id": 1,
"key": "param1",
"is_required": true
}
]
}
```
**注意**:当 business_object 未开始时current_state 为 None返回空参数列表和提示消息。
**测试**`test_current_state_parameters_api()` - 已通过
### 4.4 获取状态的参数列表
**端点**`GET /api/v1/stateflow/states/{id}/parameters/`
**查询参数**
- `required_only`: boolean默认 false- 是否只返回必填参数
**响应示例**
```json
{
"state_id": 1,
"count": 2,
"parameters": [
{
"id": 1,
"key": "param1",
"value": "value1",
"is_required": true,
"is_image_path": false,
"description": "参数描述"
}
]
}
```
**测试**`test_state_parameters_api()` - 已通过
---
## 五、测试优化
### 5.1 测试文件重组
**变更**:将 `stateflow/tests.py` 移动到 `stateflow/tests/test_services.py`
**原因**
- 解决文件与目录命名冲突
- 符合 Django 测试最佳实践
- 便于后续测试文件的组织和扩展
### 5.2 测试数据管理优化
**优化的测试类**`StateAPITestCase`
**改进内容**
- 将重复的测试数据创建移至 `setUp()` 方法
- 创建共享的基础数据:`self.param1`, `self.param2`, `self.state1`, `self.state2`
- 避免每个测试方法中重复创建相同数据
- 提高测试执行效率和可维护性
**优化前**:每个测试方法内部创建 State 和 Parameter
**优化后**:在 setUp 中创建基础数据,测试方法直接使用
### 5.3 新增测试用例
**文件**`stateflow/test_api.py` - `BusinessObjectNewAPITestCase`
包含 4 个新的 API 测试:
1. `test_next_pending_state_api()` - 测试获取下一个待执行节点
2. `test_pending_states_api()` - 测试获取所有待执行节点
3. `test_current_state_parameters_api()` - 测试获取当前状态参数
4. `test_state_parameters_api()` - 测试获取状态参数列表
**文件**`stateflow/tests/test_services.py`
新增 3 个服务层测试:
1. `test_get_next_pending_state()` - 测试获取下一个待执行节点服务
2. `test_get_all_pending_states()` - 测试获取所有待执行节点服务
3. `test_get_state_parameters()` - 测试获取状态参数服务
### 5.4 测试覆盖率
- **总测试数**43 个
- **测试状态**:全部通过 ✓
- **执行时间**:约 6.5 秒
- **覆盖模块**
- 数据模型Model
- 服务层Services
- API 接口Views
- 序列化器Serializers
---
## 六、受影响的文件清单
### 6.1 模型层
- `stateflow/models.py` - State 和 StateParameter 关系及字段变更
### 6.2 服务层
- `stateflow/services.py` - current_state 逻辑修正 + 3 个新函数
### 6.3 序列化器
- `stateflow/serializers.py` - 适配多对多关系和新字段
### 6.4 管理后台
- `stateflow/admin.py` - current_state 显示逻辑更新
### 6.5 API 视图
- `api_v1/views/stateflow/business_object.py` - 新增 3 个 action
- `api_v1/views/stateflow/state.py` - 新增 1 个 action
### 6.6 测试文件
- `stateflow/tests/test_services.py` - 服务层测试(重组 + 新增)
- `stateflow/test_api.py` - API 测试(优化 + 新增)
- `stateflow/tests/test_step_back.py` - 更新适配新语义
- `stateflow/tests/test_step_back_api.py` - 更新适配新语义
### 6.7 数据库迁移
- `0017_state_parameter_many_to_many.py`
- `0018_remove_stateparameter_name.py`
- `0019_stateparameter_is_required.py`
- `0020_stateparameter_is_image_path.py`
---
## 七、向后兼容性
### 7.1 破坏性变更
1. **StateParameter.state 字段已移除** - 不再支持一对多关系
2. **StateParameter.name 字段已移除** - 请使用 `key` 字段
3. **current_state 语义变更** - 从"下一个待执行"变为"最后完成的"
4. **移除 'in_progress' 状态** - 只保留 'not_started' 和 'completed'
### 7.2 迁移建议
- 如果代码中使用了 `business_object.get_current_state()` 期望获取"下一个待执行节点",请改用 `get_next_pending_state(business_object)`
- 如果代码中引用了 `parameter.state`,请改用 `parameter.states.all()` 获取关联的状态列表
- 如果代码中使用了 `parameter.name`,请改用 `parameter.key`
---
## 八、后续工作计划
### 8.1 待验证模块
- **api_man 模块**:需要检查其中可能存在的 State 和 StateParameter 相关接口,确保兼容本次变更
### 8.2 待扩展功能
- **printing 模块依赖更新**:验证 printing 模块对 stateflow 的依赖是否需要调整
- **参数验证逻辑**:实现基于 `is_required` 的自动验证机制
- **图片参数处理**:完善 `is_image_path` 的前后端集成
---
## 九、总结
本次更新显著提升了 stateflow 模块的灵活性和可维护性:
1. **更灵活的参数管理**:多对多关系允许参数复用
2. **更明确的语义**current_state 现在准确表示"已完成到哪里"
3. **更强大的 API**:新增 4 个接口满足不同查询需求
4. **更完善的测试**43 个测试确保代码质量
5. **更清晰的代码组织**:测试文件重组,数据管理优化
所有变更已通过完整测试验证,可以安全部署。

View File

@@ -0,0 +1,254 @@
# Printing 模块 API 清理记录
**日期**: 2025年11月15日
**目的**: 删除 printing 模块中冗余的状态流转接口,统一使用 stateflow 的业务对象接口
---
## 删除的接口
### 1. PrintingJobViewSet (印染任务)
#### 删除的方法:
**1.1 `advance_to_next_state()` 方法**
- **路径**: `POST /api/v1/printing-jobs/{id}/advance-to-next-state/`
- **原位置**: `api_v1/views/printing/views.py` 约第 309-342 行
- **删除原因**:
- 功能与 stateflow 统一接口重复
- 不支持参数传递
- 调用签名过时只接收2个返回值实际返回3个
**1.2 `step_back_one_state()` 方法**
- **路径**: `POST /api/v1/printing-jobs/{id}/step-back-one-state/`
- **原位置**: `api_v1/views/printing/views.py` 约第 345-380 行
- **删除原因**: 与 stateflow 统一接口重复
---
### 2. PlateOrderViewSet (开版订单)
#### 删除的方法:
**2.1 `advance_to_next_state()` 方法**
- **路径**: `POST /api/v1/plate-orders/{id}/advance-to-next-state/`
- **原位置**: `api_v1/views/printing/views.py` 约第 656-690 行
- **删除原因**:
- 功能与 stateflow 统一接口重复
- 不支持参数传递
- 调用签名过时只接收2个返回值实际返回3个
**2.2 `step_back_one_state()` 方法**
- **路径**: `POST /api/v1/plate-orders/{id}/step-back-one-state/`
- **原位置**: `api_v1/views/printing/views.py` 约第 693-728 行
- **删除原因**: 与 stateflow 统一接口重复
---
## 保留的接口
### PrintingJobViewSet 保留:
-`GET /api/v1/printing-jobs/{id}/completed-states/` - 查询已完成的流程列表(便捷方法)
### PlateOrderViewSet 保留:
-`GET /api/v1/plate-orders/{id}/completed-states/` - 查询已完成的流程列表(便捷方法)
---
## 迁移指南
### 旧接口 → 新接口映射
#### 推进到下一状态
**旧方式(已废弃)**:
```bash
POST /api/v1/printing-jobs/123/advance-to-next-state/
# 或
POST /api/v1/plate-orders/456/advance-to-next-state/
```
**新方式(推荐)**:
```bash
# 1. 先获取 business_object_id
GET /api/v1/printing-jobs/123/
# 或
GET /api/v1/plate-orders/456/
# 响应中包含 business_object_id 或 business_object.id
# 2. 调用 stateflow 统一接口(支持参数传递)
POST /api/v1/stateflow/business-objects/{business_object_id}/advance/
{
"parameters": {
"temperature": "25.5",
"humidity": "60%",
"operator": "张三"
}
}
```
#### 回退一步
**旧方式(已废弃)**:
```bash
POST /api/v1/printing-jobs/123/step-back-one-state/
# 或
POST /api/v1/plate-orders/456/step-back-one-state/
```
**新方式(推荐)**:
```bash
POST /api/v1/stateflow/business-objects/{business_object_id}/step_back/
```
---
## 新接口的优势
### 1. 支持参数管理
- ✅ 推进时可传递参数
- ✅ 自动校验必填参数
- ✅ 支持参数历史记录
- ✅ 支持补充参数
### 2. 统一的接口设计
- ✅ 所有业务对象使用相同接口
- ✅ 统一的错误处理
- ✅ 统一的响应格式
### 3. 更强大的功能
- ✅ 查询下一个待执行节点及参数要求
- ✅ 查询状态时间线
- ✅ 查询参数历史
- ✅ 重置进度
---
## 完整的 stateflow 接口列表
### 状态流转控制
- `POST /api/v1/stateflow/business-objects/{id}/advance/` - 推进(支持参数)
- `POST /api/v1/stateflow/business-objects/{id}/step_back/` - 回退
- `POST /api/v1/stateflow/business-objects/{id}/reset/` - 重置进度
### 状态查询
- `GET /api/v1/stateflow/business-objects/{id}/timeline/` - 状态时间线
- `GET /api/v1/stateflow/business-objects/{id}/next_pending_state/` - 下一个待执行节点
- `GET /api/v1/stateflow/business-objects/{id}/pending_states/` - 所有待执行节点
- `GET /api/v1/stateflow/business-objects/{id}/current_state_parameters/` - 当前状态参数
### 参数管理
- `POST /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/add-parameters/` - 补充参数
- `GET /api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/` - 查询参数
详见: `stateflow/BusinessObject_API.md`
---
## 模型层修改
**无需修改**
-`PlateOrder.business_object` 字段保留
-`PrintingJob.business_object` 字段保留
-`PlateOrder.status` 属性保留(便捷访问器)
-`PrintingJob.status` 属性保留(便捷访问器)
-`PlateOrder.is_completed` 属性保留
-`PrintingJob.is_completed` 属性保留
-`PlateOrder.has_started` 属性保留
-`PrintingJob.has_started` 属性保留
这些模型方法和属性设计正确,无需调整。
---
## 识别的问题(已修复)
### 问题 1: 调用签名不匹配 ❌
**位置**:
- `api_v1/views/printing/views.py` 第 325、672 行
- `api_v1/views/printing/views.py` 第 360、708 行
**问题代码**:
```python
# ❌ 错误:只接收 2 个返回值
success, message = stateflow_services.advance_to_next_state(
job.business_object, request.user
)
```
**实际签名**:
```python
# ✅ 正确:返回 3 个值
def advance_to_next_state(...) -> Tuple[bool, str, Optional['models.StateFlowRecord']]:
return success, message, state_log
```
**解决方案**: 删除这些接口,使用 stateflow 统一接口 ✅
### 问题 2: 缺少参数支持 ❌
- printing 模块的接口不接受 parameters
- 无法传递状态参数
- 不支持必填参数校验
**解决方案**: 使用 stateflow 统一接口(完整支持参数管理)✅
---
## 测试状态
- ❌ printing 模块测试失败(与本次修改无关)
- 原因: PlateOrder 模型缺少 `plate_code` 字段
- 建议: 修复 PlateOrder 模型定义或测试用例
- ✅ stateflow 模块测试全部通过55个测试
---
## 后续建议
### 1. 更新前端代码
- 将所有调用 printing-jobs/advance 的代码改为调用 stateflow API
- 将所有调用 plate-orders/advance 的代码改为调用 stateflow API
### 2. 文档更新
- ✅ 已创建 `stateflow/BusinessObject_API.md`
- 建议: 在 printing 模块文档中添加迁移说明
### 3. 修复 printing 测试
- PlateOrder 模型缺少 `plate_code` 字段定义
- 或者测试用例使用了错误的字段名
### 4. 考虑添加废弃警告(可选)
如果需要渐进式迁移,可以暂时保留旧接口但返回废弃警告:
```python
@action(detail=True, methods=['post'])
def advance_to_next_state(self, request, pk=None):
return Response({
'error': 'This endpoint is deprecated. Please use /api/v1/stateflow/business-objects/{id}/advance/'
}, status=status.HTTP_410_GONE)
```
---
## 影响范围
### 代码变更
- ✅ 删除 4 个 API 方法
- ✅ 文件行数减少约 138 行
- ✅ 无破坏性变更(只要前端同步更新)
### API 变更
-**破坏性变更**: 删除了 4 个 API 端点
- ✅ 替代方案: stateflow 统一接口功能更强
### 数据库变更
- ✅ 无需数据库迁移
- ✅ 无数据丢失风险
---
**变更完成时间**: 2025-11-15
**执行人**: AI Assistant
**审核状态**: 待人工审核

View File

@@ -4,21 +4,55 @@ from django.utils.safestring import mark_safe
from . import models from . import models
class StateParameterInline(admin.StackedInline): @admin.register(models.StateParameter)
model = models.StateParameter class StateParameterAdmin(admin.ModelAdmin):
extra = 1 """工艺参数管理"""
fields = ('key', 'value', 'attachment', 'description') list_display = (
readonly_fields = ('attachment_preview',) 'id',
'key',
'value',
'is_required',
'is_image_path',
'state_list',
'description',
'attachment_preview',
)
search_fields = ('key', 'description')
list_filter = ('is_required', 'is_image_path', 'created_at', 'updated_at', 'states__processes__name')
readonly_fields = ('attachment_preview_detail', 'value_preview')
@admin.display(description='附件')
def attachment_preview(self, obj): def attachment_preview(self, obj):
"""显示附件预览""" """列表中显示是否有附件"""
if obj.attachment:
return ''
return ''
@admin.display(description='所属节点')
def state_list(self, obj):
"""显示关联的状态节点"""
states = obj.states.all()
if states:
return mark_safe('<br>'.join([f"{state.name}" for state in states]))
return '-'
def attachment_preview_detail(self, obj):
"""详情页显示附件预览"""
if obj.attachment: if obj.attachment:
if obj.attachment.name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp')): if obj.attachment.name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp')):
return mark_safe(f'<img src="{obj.attachment.url}" style="max-width: 200px; max-height: 200px;" />') return mark_safe(f'<img src="{obj.attachment.url}" style="max-width: 400px; max-height: 400px;" />')
else: else:
return mark_safe(f'<a href="{obj.attachment.url}" target="_blank">查看附件</a>') return mark_safe(f'<a href="{obj.attachment.url}" target="_blank">下载附件</a>')
return "-" return "-"
attachment_preview.short_description = '附件预览' attachment_preview_detail.short_description = '附件预览'
def value_preview(self, obj):
"""如果标记为图片路径,则显示图片预览"""
if obj.is_image_path and obj.value:
# 假设 value 是图片 URL
return mark_safe(f'<img src="{obj.value}" style="max-width: 400px; max-height: 400px;" onerror="this.style.display=\'none\'; this.parentNode.innerHTML+=\'<span>(图片加载失败)</span>\';" />')
return obj.value or "-"
value_preview.short_description = 'Value预览'
class ProcessNodeInline(admin.StackedInline): class ProcessNodeInline(admin.StackedInline):
@@ -36,25 +70,22 @@ class StateAdmin(admin.ModelAdmin):
'description', 'description',
'created_at', 'created_at',
'updated_at', 'updated_at',
'parameter_overview', 'parameter_count',
) )
search_fields = ('name', 'description') search_fields = ('name', 'description')
list_filter = ('created_at', 'updated_at', 'processes__name') list_filter = ('created_at', 'updated_at', 'processes__name')
inlines = [StateParameterInline] filter_horizontal = ('parameters',)
@admin.display(description='参数概览') @admin.display(description='参数数量')
def parameter_overview(self, obj: models.State): def parameter_count(self, obj: models.State):
params = obj.parameters.all() count = obj.parameters.count()
if params: if count > 0:
result = [] params = obj.parameters.all()[:3]
for param in params: keys = ', '.join([p.key for p in params])
param_str = f"{param.key}={param.value}" if count > 3:
if param.attachment: keys += f'{count}'
param_str += f' <a href="{param.attachment.url}" target="_blank">📎</a>' return f"{count} ({keys})"
result.append(param_str) return "0"
return mark_safe("<br>".join(result))
else:
return "-"
@admin.register(models.Process) @admin.register(models.Process)
@@ -125,7 +156,7 @@ class BusinessObjectAdmin(admin.ModelAdmin):
messages = [] messages = []
for obj in queryset: for obj in queryset:
success, message = services.advance_to_next_state(obj, request.user) success, message, _ = services.advance_to_next_state(obj, request.user)
if success: if success:
success_count += 1 success_count += 1
messages.append(f"{obj.name}: {message}") messages.append(f"{obj.name}: {message}")
@@ -162,17 +193,19 @@ class BusinessObjectAdmin(admin.ModelAdmin):
if overall_status == 'not_started': if overall_status == 'not_started':
return '未开始' return '未开始'
elif overall_status == 'completed': elif overall_status == 'completed':
return '已完成' return f'已完成 ({current_state.name if current_state else "-"})'
elif current_state: elif current_state:
return current_state.name # 显示最后完成的状态
return f'进行中 (已完成: {current_state.name})'
return '-' return '-'
@admin.display(description='进度') @admin.display(description='进度')
def progress(self, obj: models.BusinessObject): def progress(self, obj: models.BusinessObject):
return f"{obj.get_progress_percentage():.1f}%" return f"{obj.get_progress_percentage():.1f}%"
@admin.display(description='本步骤参数') @admin.display(description='最后完成状态参数')
def params(self, obj: models.BusinessObject): def params(self, obj: models.BusinessObject):
"""显示最后完成状态的参数"""
current_state = obj.get_current_state() current_state = obj.get_current_state()
if current_state: if current_state:
params = current_state.parameters.all() params = current_state.parameters.all()

View File

@@ -0,0 +1,70 @@
# Generated manually for stateflow parameter refactoring
from django.db import migrations, models
def cleanup_old_parameters(apps, schema_editor):
"""
清空旧的参数数据(测试数据,无需保留)
"""
StateParameter = apps.get_model('stateflow', 'StateParameter')
# 删除所有旧的参数
StateParameter.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0016_alter_stateparameter_key_alter_stateparameter_value'),
]
operations = [
# 步骤1: 清空旧参数数据
migrations.RunPython(
cleanup_old_parameters,
migrations.RunPython.noop,
),
# 步骤2: 移除旧的 ForeignKey
migrations.RemoveField(
model_name='stateparameter',
name='state',
),
# 步骤3: 添加 name 字段
migrations.AddField(
model_name='stateparameter',
name='name',
field=models.CharField(
default='未命名参数',
help_text='参数的显示名称',
max_length=100,
verbose_name='参数名称'
),
preserve_default=False,
),
# 步骤4: 添加多对多字段
migrations.AddField(
model_name='state',
name='parameters',
field=models.ManyToManyField(
blank=True,
help_text='此状态使用的工艺参数',
related_name='states',
to='stateflow.stateparameter',
verbose_name='关联参数'
),
),
# 步骤5: 将 key 设置为 unique
migrations.AlterField(
model_name='stateparameter',
name='key',
field=models.CharField(
help_text='唯一标识符',
max_length=100,
unique=True,
verbose_name='参数键'
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-11-15 05:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0017_state_parameter_many_to_many'),
]
operations = [
migrations.RemoveField(
model_name='stateparameter',
name='name',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-15 05:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0018_remove_stateparameter_name'),
]
operations = [
migrations.AddField(
model_name='stateparameter',
name='is_required',
field=models.BooleanField(default=False, help_text='标记该参数是否必须提供', verbose_name='是否必填'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-15 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0019_stateparameter_is_required'),
]
operations = [
migrations.AddField(
model_name='stateparameter',
name='is_image_path',
field=models.BooleanField(default=False, help_text='标记value字段是否为图片URL路径', verbose_name='是否图片路径'),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.2.7 on 2025-11-15 08:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0020_stateparameter_is_image_path'),
]
operations = [
migrations.CreateModel(
name='StateLogParameterRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('parameters', models.JSONField(help_text='JSON格式存储参数例如: {"temperature": "25.5", "operator": "张三"}', verbose_name='参数数据')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('remark', models.TextField(blank=True, default='', verbose_name='备注')),
('state_log', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameter_records', to='stateflow.stateflowrecord', verbose_name='状态流转记录')),
],
options={
'verbose_name': '状态流转参数记录',
'verbose_name_plural': '状态流转参数记录',
'db_table': 'state_log_parameter_record',
'ordering': ['created_at'],
'indexes': [models.Index(fields=['state_log', 'created_at'], name='state_log_p_state_l_8cc99f_idx')],
},
),
]

View File

@@ -5,11 +5,34 @@ from flower.common import ModelBase
from typing import List from typing import List
class StateParameter(ModelBase):
"""StateParameter model representing parameters that can be shared across states."""
key = models.CharField(max_length=100, unique=True, verbose_name='参数键', help_text='唯一标识符')
value = models.CharField(max_length=200, blank=True, null=True, verbose_name='参数值')
attachment = models.FileField(upload_to='state_parameters/', null=True, blank=True, verbose_name='附件')
description = models.CharField(max_length=200, blank=True, verbose_name='参数描述')
is_required = models.BooleanField(default=False, verbose_name='是否必填', help_text='标记该参数是否必须提供')
is_image_path = models.BooleanField(default=False, verbose_name='是否图片路径', help_text='标记value字段是否为图片URL路径')
def __str__(self):
return self.key
class Meta:
verbose_name = '工艺参数'
verbose_name_plural = '工艺参数'
class State(ModelBase): class State(ModelBase):
"""State model representing a state in a stateflow diagram.""" """State model representing a state in a stateflow diagram."""
name = models.CharField(max_length=100, verbose_name='状态名称') name = models.CharField(max_length=100, verbose_name='状态名称')
# previous = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上一级状态')
description = models.CharField(max_length=200, blank=True, verbose_name='状态描述') description = models.CharField(max_length=200, blank=True, verbose_name='状态描述')
parameters = models.ManyToManyField(
StateParameter,
blank=True,
related_name='states',
verbose_name='关联参数',
help_text='此状态使用的工艺参数'
)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -25,27 +48,26 @@ class State(ModelBase):
"""Retrieve additional parameters from the state's data field.""" """Retrieve additional parameters from the state's data field."""
return self.data if self.data else {} return self.data if self.data else {}
def get_parameters(self, required_only: bool = False) -> List['StateParameter']:
"""
获取状态关联的参数列表
Args:
required_only: 是否只返回必填参数,默认 False 返回所有参数
Returns:
参数列表
"""
params = self.parameters.all()
if required_only:
params = params.filter(is_required=True)
return list(params)
class Meta: class Meta:
verbose_name = '流程节点' verbose_name = '流程节点'
verbose_name_plural = '流程节点' verbose_name_plural = '流程节点'
class StateParameter(ModelBase):
"""StateParameter model representing parameters associated with a state."""
state = models.ForeignKey(State, on_delete=models.PROTECT, related_name='parameters', verbose_name='关联参数')
key = models.CharField(max_length=100, blank=True, null=True, verbose_name='参数键')
value = models.CharField(max_length=200, blank=True, null=True, verbose_name='参数值')
attachment = models.FileField(upload_to='state_parameters/', null=True, blank=True, verbose_name='附件')
description = models.CharField(max_length=200, blank=True, verbose_name='参数描述')
def __str__(self):
return f"{self.state.name} - {self.key}"
class Meta:
verbose_name = '工艺参数'
verbose_name_plural = '工艺参数'
class Process(ModelBase): class Process(ModelBase):
"""Process model representing a stateflow process template.""" """Process model representing a stateflow process template."""
name = models.CharField(max_length=100, verbose_name='流程名称') name = models.CharField(max_length=100, verbose_name='流程名称')
@@ -152,9 +174,132 @@ class StateFlowRecord(ModelBase):
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='撤销时间') cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='撤销时间')
is_cancelled = models.BooleanField(default=False, verbose_name='是否已撤销') is_cancelled = models.BooleanField(default=False, verbose_name='是否已撤销')
def get_latest_parameter(self, key, default=None, include_cancelled=False):
"""
获取参数的最新值(最后一次提交的值)
参数:
key: 参数键
default: 默认值
include_cancelled: 是否包含已撤销状态的参数记录,默认 False
返回:
参数值或默认值
"""
# 如果状态已撤销且不包含已撤销记录,返回默认值
if self.is_cancelled and not include_cancelled:
return default
records = self.parameter_records.filter(
parameters__has_key=key
).order_by('-created_at')
if records.exists():
return records.first().parameters.get(key, default)
return default
def get_parameter_history(self, key, include_cancelled=False):
"""
获取参数的完整历史
参数:
key: 参数键
include_cancelled: 是否包含已撤销状态的参数记录,默认 False
返回:
list: 历史记录列表,每项包含 value, created_at, remark
"""
# 如果状态已撤销且不包含已撤销记录,返回空列表
if self.is_cancelled and not include_cancelled:
return []
records = self.parameter_records.filter(
parameters__has_key=key
).order_by('created_at')
history = []
for record in records:
history.append({
'value': record.parameters.get(key),
'created_at': record.created_at,
'remark': record.remark
})
return history
def get_all_parameters_summary(self, include_cancelled=False):
"""
获取所有参数的最新值汇总
参数:
include_cancelled: 是否包含已撤销状态的参数记录,默认 False
返回:
dict: {key: value} 每个key取最新的值
"""
# 如果状态已撤销且不包含已撤销记录,返回空字典
if self.is_cancelled and not include_cancelled:
return {}
summary = {}
# 按时间顺序,后面的记录覆盖前面的
for record in self.parameter_records.all().order_by('created_at'):
summary.update(record.parameters)
return summary
def get_all_parameter_records_count(self):
"""获取参数记录总数"""
return self.parameter_records.count()
class Meta: class Meta:
db_table = 'state_flow_record' db_table = 'state_flow_record'
verbose_name = '状态流转记录' verbose_name = '状态流转记录'
verbose_name_plural = '状态流转记录' verbose_name_plural = '状态流转记录'
# 移除 unique_together 约束,允许同一状态多次记录(支持撤销后重新执行) # 移除 unique_together 约束,允许同一状态多次记录(支持撤销后重新执行)
ordering = ['completed_at'] ordering = ['completed_at']
class StateLogParameterRecord(ModelBase):
"""
状态流转参数记录
使用 JSON 存储一次提交的所有参数
支持对同一状态流转多次补充参数
"""
state_log = models.ForeignKey(
StateFlowRecord,
on_delete=models.CASCADE,
related_name='parameter_records',
verbose_name='状态流转记录'
)
parameters = models.JSONField(
verbose_name='参数数据',
help_text='JSON格式存储参数例如: {"temperature": "25.5", "operator": "张三"}'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
remark = models.TextField(blank=True, default='', verbose_name='备注')
def __str__(self):
return f"{self.state_log} - {len(self.parameters)} 个参数 ({self.created_at})"
def get_parameter(self, key, default=None):
"""快捷方式:获取单个参数值"""
return self.parameters.get(key, default)
def has_parameter(self, key):
"""快捷方式:检查参数是否存在"""
return key in self.parameters
def get_all_keys(self):
"""快捷方式:获取所有参数键"""
return list(self.parameters.keys())
class Meta:
db_table = 'state_log_parameter_record'
verbose_name = '状态流转参数记录'
verbose_name_plural = '状态流转参数记录'
ordering = ['created_at']
indexes = [
models.Index(fields=['state_log', 'created_at']),
]

View File

@@ -13,7 +13,7 @@ class StateParameterSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.StateParameter model = models.StateParameter
fields = ['id', 'key', 'value', 'attachment', 'attachment_url', 'description'] fields = ['id', 'key', 'value', 'attachment', 'attachment_url', 'description', 'is_required', 'is_image_path']
read_only_fields = ['id', 'attachment_url'] read_only_fields = ['id', 'attachment_url']
def get_attachment_url(self, obj): def get_attachment_url(self, obj):
@@ -47,39 +47,50 @@ class StateDetailSerializer(serializers.ModelSerializer):
class StateCreateUpdateSerializer(serializers.ModelSerializer): class StateCreateUpdateSerializer(serializers.ModelSerializer):
"""状态创建/更新序列化器""" """状态创建/更新序列化器"""
parameters = StateParameterSerializer(many=True, required=False) parameter_ids = serializers.ListField(
child=serializers.IntegerField(),
required=False,
write_only=True,
help_text="参数ID列表"
)
parameters = StateParameterSerializer(many=True, read_only=True)
class Meta: class Meta:
model = models.State model = models.State
fields = ['name', 'description', 'parameters'] fields = ['name', 'description', 'parameter_ids', 'parameters']
def validate_parameter_ids(self, value):
"""验证参数ID是否存在"""
if value:
existing_ids = set(models.StateParameter.objects.filter(id__in=value).values_list('id', flat=True))
invalid_ids = set(value) - existing_ids
if invalid_ids:
raise serializers.ValidationError(f"参数ID不存在: {invalid_ids}")
return value
@transaction.atomic @transaction.atomic
def create(self, validated_data): def create(self, validated_data):
parameters_data = validated_data.pop('parameters', []) parameter_ids = validated_data.pop('parameter_ids', [])
state = models.State.objects.create(**validated_data) state = models.State.objects.create(**validated_data)
# 创建参数 # 关联参数(多对多)
for param_data in parameters_data: if parameter_ids:
models.StateParameter.objects.create(state=state, **param_data) state.parameters.set(parameter_ids)
return state return state
@transaction.atomic @transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
parameters_data = validated_data.pop('parameters', None) parameter_ids = validated_data.pop('parameter_ids', None)
# 更新基本字段 # 更新基本字段
instance.name = validated_data.get('name', instance.name) instance.name = validated_data.get('name', instance.name)
instance.description = validated_data.get('description', instance.description) instance.description = validated_data.get('description', instance.description)
instance.save() instance.save()
# 如果提供了参数数据,更新参数 # 如果提供了参数ID列表,更新关联
if parameters_data is not None: if parameter_ids is not None:
# 删除旧参数 instance.parameters.set(parameter_ids)
instance.parameters.all().delete()
# 创建新参数
for param_data in parameters_data:
models.StateParameter.objects.create(state=instance, **param_data)
return instance return instance
@@ -180,6 +191,15 @@ class ProcessCreateUpdateSerializer(serializers.ModelSerializer):
# BusinessObject 序列化器 # BusinessObject 序列化器
class StateLogParameterRecordSerializer(serializers.ModelSerializer):
"""状态流转参数记录序列化器"""
class Meta:
model = models.StateLogParameterRecord
fields = ['id', 'state_log', 'parameters', 'created_at', 'remark']
read_only_fields = ['id', 'created_at']
class StateFlowRecordSerializer(serializers.ModelSerializer): class StateFlowRecordSerializer(serializers.ModelSerializer):
"""状态流转记录序列化器(只读)""" """状态流转记录序列化器(只读)"""
state_name = serializers.CharField(source='state.name', read_only=True) state_name = serializers.CharField(source='state.name', read_only=True)
@@ -195,6 +215,27 @@ class StateFlowRecordSerializer(serializers.ModelSerializer):
read_only_fields = ['id', 'completed_at', 'is_cancelled', 'cancelled_at'] read_only_fields = ['id', 'completed_at', 'is_cancelled', 'cancelled_at']
class StateFlowRecordWithParametersSerializer(serializers.ModelSerializer):
"""状态流转记录序列化器(包含参数摘要)"""
state_name = serializers.CharField(source='state.name', read_only=True)
completed_by_username = serializers.CharField(source='completed_by.username', read_only=True)
parameters_summary = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.StateFlowRecord
fields = [
'id', 'state', 'state_name', 'completed_at',
'completed_by', 'completed_by_username',
'is_cancelled', 'cancelled_at',
'parameters_summary'
]
read_only_fields = ['id', 'completed_at', 'is_cancelled', 'cancelled_at']
def get_parameters_summary(self, obj):
"""获取参数摘要"""
return obj.get_all_parameters_summary()
class BusinessObjectListSerializer(serializers.ModelSerializer): class BusinessObjectListSerializer(serializers.ModelSerializer):
"""业务对象列表序列化器""" """业务对象列表序列化器"""
process_name = serializers.CharField(source='process.name', read_only=True) process_name = serializers.CharField(source='process.name', read_only=True)

View File

@@ -11,56 +11,45 @@ User = get_user_model()
def get_business_object_current_state(business_object: 'models.BusinessObject') -> Optional['models.State']: def get_business_object_current_state(business_object: 'models.BusinessObject') -> Optional['models.State']:
""" """
获取订单的当前状态 获取业务对象的当前状态(最后完成的状态)
规则: 规则:
1. 如果没有任何完成记录(或所有记录都被撤销),返回 None未开始处理 1. 如果没有任何完成记录(或所有记录都被撤销),返回 None未开始
2. 如果有完成的节点,返回第一个未完成的节点(进行中) 2. 如果有完成记录,返回最后完成的状态(进行中或已完成
3. 如果所有节点都已完成,返回 None流程已完成
"""
# 获取流程的所有节点(按顺序)
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
if not process_nodes.exists():
return None
# 获取已完成且未被撤销的状态ID集合 注意current_state 表示"最后完成的状态",而不是"待执行的状态"
completed_state_ids = set( """
business_object.state_logs.filter(is_cancelled=False).values_list('state_id', flat=True) # 获取最后一条完成且未撤销的记录
last_completed_record = (
business_object.state_logs
.filter(is_cancelled=False)
.select_related('state')
.order_by('-completed_at', '-id')
.first()
) )
# 如果没有任何有效完成记录,返回 None未开始 if last_completed_record:
if not completed_state_ids: return last_completed_record.state
return None
# 找到第一个未完成的节点 # 没有任何完成记录,返回 None未开始
for node in process_nodes:
if node.state_id not in completed_state_ids:
return node.state
# 所有节点都已完成,返回 None已完成
return None return None
def get_business_object_state_status(business_object: 'models.BusinessObject', state: 'models.State') -> str: def get_business_object_state_status(business_object: 'models.BusinessObject', state: 'models.State') -> str:
""" """
获取订单中某个状态的状态 获取业务对象中某个状态的状态
返回值: 返回值:
- 'not_started': 未开始 - 'not_started': 未开始
- 'in_progress': 进行中
- 'completed': 已完成 - 'completed': 已完成
注意:由于 current_state 现在表示"最后完成的状态",不再有 'in_progress' 概念
""" """
# 检查是否已完成且未被撤销 # 检查是否已完成且未被撤销
is_completed = business_object.state_logs.filter(state=state, is_cancelled=False).exists() is_completed = business_object.state_logs.filter(state=state, is_cancelled=False).exists()
if is_completed: if is_completed:
return 'completed' return 'completed'
current_state = get_business_object_current_state(business_object)
# 检查是否为当前状态
if current_state and current_state.id == state.id:
return 'in_progress'
return 'not_started' return 'not_started'
@@ -83,50 +72,64 @@ def get_progress_percentage(business_object: 'models.BusinessObject') -> float:
return (completed_count / total_nodes) * 100 return (completed_count / total_nodes) * 100
def advance_to_next_state(business_object: 'models.BusinessObject', user) -> Tuple[bool, str]: def advance_to_next_state(business_object: 'models.BusinessObject', user, **parameters) -> Tuple[bool, str, Optional['models.StateFlowRecord']]:
""" """
订单推进到下一个状态 业务对象推进到下一个状态(完成下一个未完成的节点)
返回: (是否成功, 消息) 参数:
business_object: BusinessObject 实例
user: 操作用户
**parameters: 状态参数(可选)
返回:
tuple: (success: bool, message: str, state_log: StateFlowRecord|None)
- success: 是否成功
- message: 提示信息
- state_log: 创建的状态日志(成功时)
""" """
can_advance, reason = can_advance_to_next_state(business_object) can_advance, reason = can_advance_to_next_state(business_object)
if not can_advance: if not can_advance:
return False, reason return False, reason, None
with transaction.atomic(): with transaction.atomic():
# 获取当前状态(返回第一个未完成的节点 # 获取下一个待执行的节点
current_state = get_business_object_current_state(business_object) next_pending = get_next_pending_state(business_object, include_parameters=False)
# 如果当前状态为 None说明是初始状态未开始 if not next_pending:
if current_state is None: return False, "没有待执行的节点", None
# 获取第一个节点
first_node = business_object.process.process_nodes.order_by('order', 'id').first()
if not first_node:
return False, "流程没有任何节点"
# 标记第一个状态为已完成
models.StateFlowRecord.objects.create(
business_object=business_object,
state=first_node.state,
completed_by=user
)
return True, f"已完成状态: {first_node.state.name}"
# current_state 不为 None 时,它一定是未完成的节点 next_state = next_pending['state']
# 标记当前状态为已完成
models.StateFlowRecord.objects.create( # 验证必填参数
if parameters:
is_valid, missing_params = validate_required_parameters(next_state, **parameters)
if not is_valid:
return False, f"缺失必填参数: {', '.join(missing_params)}", None
else:
# 检查是否有必填参数
required_params = get_required_parameters(next_state)
if required_params.exists():
required_keys = list(required_params.values_list('key', flat=True))
return False, f"缺失必填参数: {', '.join(required_keys)}", None
# 创建状态流转记录
state_log = models.StateFlowRecord.objects.create(
business_object=business_object, business_object=business_object,
state=current_state, state=next_state,
completed_by=user completed_by=user
) )
# 检查是否所有状态都已完成 # 如果提供了参数,创建参数记录
next_state = get_business_object_current_state(business_object) if parameters:
if next_state is None: create_parameter_record(state_log, **parameters)
# 所有状态都已完成
return True, f"流程已完成,最后状态: {current_state.name}"
return True, f"已完成状态: {current_state.name}" # 检查是否所有状态都已完成
after_advance = get_next_pending_state(business_object, include_parameters=False)
if after_advance is None:
# 所有状态都已完成
return True, f"流程已完成,最后状态: {next_state.name}", state_log
return True, f"已完成状态: {next_state.name}", state_log
def reset_business_object_progress(business_object: 'models.BusinessObject') -> None: def reset_business_object_progress(business_object: 'models.BusinessObject') -> None:
@@ -189,23 +192,24 @@ def get_current_state_parameters(business_object: 'models.BusinessObject') -> Li
def get_overall_status(business_object: 'models.BusinessObject') -> str: def get_overall_status(business_object: 'models.BusinessObject') -> str:
""" """
获取订单的整体状态 获取业务对象的整体状态
返回值: 返回值:
- 'not_started': 未开始(没有任何有效的完成记录) - 'not_started': 未开始(没有任何有效的完成记录)
- 'in_progress': 进行中(有部分状态已完成) - 'in_progress': 进行中(有部分状态已完成,但未完成所有
- 'completed': 已完成(所有状态都已完成) - 'completed': 已完成(所有状态都已完成)
""" """
current_state = get_business_object_current_state(business_object)
# 检查是否有任何有效的完成记录 # 检查是否有任何有效的完成记录
has_completed = business_object.state_logs.filter(is_cancelled=False).exists() has_completed = business_object.state_logs.filter(is_cancelled=False).exists()
if not has_completed: if not has_completed:
return 'not_started' return 'not_started'
if current_state is None: # 检查是否有下一个待执行节点
# 有完成记录,但当前状态为 None说明所有状态都已完成 next_pending = get_next_pending_state(business_object, include_parameters=False)
if next_pending is None:
# 没有待执行节点,说明所有状态都已完成
return 'completed' return 'completed'
return 'in_progress' return 'in_progress'
@@ -213,7 +217,7 @@ def get_overall_status(business_object: 'models.BusinessObject') -> str:
def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple[bool, str]: def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple[bool, str]:
""" """
检查订单是否可以推进到下一个状态 检查业务对象是否可以推进到下一个状态
返回: (是否可以推进, 原因) 返回: (是否可以推进, 原因)
""" """
@@ -222,35 +226,27 @@ def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple
if not first_node: if not first_node:
return False, "流程没有任何节点" return False, "流程没有任何节点"
current_state = get_business_object_current_state(business_object) # 获取下一个待执行节点
next_pending = get_next_pending_state(business_object, include_parameters=False)
# 如果当前状态为 None有两种情况 if next_pending is None:
# 1. 没有完成记录 - 未开始 # 没有待执行节点,流程已完成
# 2. 所有节点已完成 - 已完成 return False, "流程已完成,无法继续推进"
if current_state is None:
completed_count = business_object.state_logs.filter(is_cancelled=False).count()
if completed_count == 0:
# 未开始,可以推进到第一个状态
return True, f"可以开始处理,将推进到: {first_node.state.name}"
else:
# 所有状态都已完成
return False, "流程已完成,无法继续推进"
# current_state 不为 None 时,它一定是第一个未完成的节点,可以推进 next_state = next_pending['state']
return True, f"可以推进到: {current_state.name}" return True, f"可以推进到: {next_state.name}"
def get_business_object_state_timeline(business_object: 'models.BusinessObject') -> List[dict]: def get_business_object_state_timeline(business_object: 'models.BusinessObject') -> List[dict]:
""" """
获取订单状态时间线(包括未开始、进行中和已完成的状态) 获取业务对象状态时间线(包括未开始和已完成的状态)
返回格式: 返回格式:
[ [
{ {
'state': State对象, 'state': State对象,
'status': 'not_started' | 'in_progress' | 'completed' | 'cancelled', 'status': 'not_started' | 'completed' | 'cancelled',
'business_object': 顺序号, 'order': 顺序号,
'completed_at': 完成时间(如果已完成), 'completed_at': 完成时间(如果已完成),
'completed_by': 完成人(如果已完成), 'completed_by': 完成人(如果已完成),
'cancelled_at': 撤销时间(如果已撤销), 'cancelled_at': 撤销时间(如果已撤销),
@@ -261,7 +257,6 @@ def get_business_object_state_timeline(business_object: 'models.BusinessObject')
""" """
timeline = [] timeline = []
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id') process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
current_state = get_business_object_current_state(business_object)
# 构建状态日志映射(包括已撤销的记录) # 构建状态日志映射(包括已撤销的记录)
state_logs_map = { state_logs_map = {
@@ -282,12 +277,6 @@ def get_business_object_state_timeline(business_object: 'models.BusinessObject')
completed_by = log.completed_by completed_by = log.completed_by
cancelled_at = log.cancelled_at cancelled_at = log.cancelled_at
is_cancelled = log.is_cancelled is_cancelled = log.is_cancelled
elif current_state and state.id == current_state.id:
status = 'in_progress'
completed_at = None
completed_by = None
cancelled_at = None
is_cancelled = False
else: else:
status = 'not_started' status = 'not_started'
completed_at = None completed_at = None
@@ -306,3 +295,169 @@ def get_business_object_state_timeline(business_object: 'models.BusinessObject')
}) })
return timeline return timeline
def get_next_pending_state(
business_object: 'models.BusinessObject',
include_parameters: bool = True
) -> Optional[dict]:
"""
获取指定业务对象的下一个待执行节点(第一个未完成的节点)
Args:
business_object: 业务对象
include_parameters: 是否包含参数列表,默认 True
Returns:
包含状态信息的字典,如果没有待执行节点则返回 None
{
'state': State对象,
'order': 节点顺序号,
'parameters': [StateParameter列表] (如果 include_parameters=True)
}
"""
# 获取所有节点(按顺序)
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
if not process_nodes.exists():
return None
# 获取已完成且未被撤销的状态ID集合
completed_state_ids = set(
business_object.state_logs.filter(is_cancelled=False).values_list('state_id', flat=True)
)
# 找到第一个未完成的节点
for node in process_nodes:
if node.state_id not in completed_state_ids:
result = {
'state': node.state,
'order': node.order,
}
if include_parameters:
result['parameters'] = list(node.state.parameters.all())
return result
# 所有节点都已完成
return None
def get_all_pending_states(business_object: 'models.BusinessObject') -> List[dict]:
"""
获取指定业务对象所有待执行的节点列表(不包含参数)
Args:
business_object: 业务对象
Returns:
待执行节点列表,每个元素包含:
{
'state': State对象,
'order': 节点顺序号
}
"""
# 获取所有节点
process_nodes = business_object.process.process_nodes.select_related('state').order_by('order', 'id')
# 获取已完成且未被撤销的状态ID集合
completed_state_ids = set(
business_object.state_logs.filter(is_cancelled=False).values_list('state_id', flat=True)
)
# 找到所有未完成的节点
pending_nodes = []
for node in process_nodes:
if node.state_id not in completed_state_ids:
pending_nodes.append({
'state': node.state,
'order': node.order,
})
return pending_nodes
def get_state_parameters(
state: 'models.State',
required_only: bool = False
) -> List['models.StateParameter']:
"""
获取指定节点的所有参数列表
Args:
state: 状态节点
required_only: 是否仅包含必填参数,默认 False
Returns:
参数列表
"""
return state.get_parameters(required_only=required_only)
def get_required_parameters(state: 'models.State'):
"""
获取状态的所有必填参数
参数:
state: State 实例
返回:
QuerySet: 必填的 StateParameter 对象
"""
return state.parameters.filter(is_required=True)
def validate_required_parameters(state: 'models.State', **kwargs) -> Tuple[bool, List[str]]:
"""
验证必填参数是否全部提供
参数:
state: State 实例
**kwargs: 用户提供的参数字典
返回:
tuple: (is_valid: bool, missing_params: list)
- is_valid: 是否通过验证
- missing_params: 缺失的必填参数 key 列表
"""
required_params = get_required_parameters(state)
required_keys = list(required_params.values_list('key', flat=True))
provided_keys = set(kwargs.keys())
missing_keys = [key for key in required_keys if key not in provided_keys]
is_valid = len(missing_keys) == 0
return is_valid, missing_keys
def create_parameter_record(state_log: 'models.StateFlowRecord', remark: str = '', **parameters) -> 'models.StateLogParameterRecord':
"""
为状态流转记录创建参数记录
参数:
state_log: StateFlowRecord 实例
remark: 备注
**parameters: 参数字典(直接保存,不做验证)
返回:
StateLogParameterRecord: 创建的参数记录
"""
record = models.StateLogParameterRecord.objects.create(
state_log=state_log,
parameters=parameters, # 直接保存为 JSON
remark=remark
)
return record
def add_parameters_to_state_log(state_log: 'models.StateFlowRecord', remark: str = '', **parameters) -> 'models.StateLogParameterRecord':
"""
为已有的状态流转记录补充参数支持重复key
参数:
state_log: StateFlowRecord 实例
remark: 备注
**parameters: 参数字典
返回:
StateLogParameterRecord: 创建的参数记录
"""
return create_parameter_record(state_log, remark, **parameters)

View File

@@ -17,34 +17,41 @@ class StateAPITestCase(TestCase):
"""测试 State API""" """测试 State API"""
def setUp(self): def setUp(self):
"""设置测试数据"""
self.client = APIClient() self.client = APIClient()
self.user = User.objects.create_user(username='testuser', password='testpass') self.user = User.objects.create_user(username='testuser', password='testpass')
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
# 创建基础参数供测试使用
self.param1 = models.StateParameter.objects.create(
key='param1', value='value1', description='参数1'
)
self.param2 = models.StateParameter.objects.create(
key='param2', value='value2', description='参数2'
)
# 创建基础状态供测试使用
self.state1 = models.State.objects.create(name='状态1', description='描述1')
self.state2 = models.State.objects.create(name='状态2', description='描述2')
def test_create_state(self): def test_create_state(self):
"""测试创建状态""" """测试创建状态"""
data = { data = {
'name': '测试状态', 'name': '测试状态',
'description': '这是一个测试状态', 'description': '这是一个测试状态',
'parameters': [ 'parameter_ids': [self.param1.id, self.param2.id]
{'key': 'param1', 'value': 'value1', 'description': '参数1'},
{'key': 'param2', 'value': 'value2', 'description': '参数2'},
]
} }
response = self.client.post('/api/v1/stateflow/states/', data, format='json') response = self.client.post('/api/v1/stateflow/states/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试状态') self.assertEqual(response.data['name'], '测试状态')
# 验证参数已创建 # 验证参数已关联
state = models.State.objects.get(name='测试状态') state = models.State.objects.get(name='测试状态')
self.assertEqual(state.parameters.count(), 2) self.assertEqual(state.parameters.count(), 2)
def test_list_states(self): def test_list_states(self):
"""测试获取状态列表""" """测试获取状态列表"""
models.State.objects.create(name='状态1', description='描述1')
models.State.objects.create(name='状态2', description='描述2')
response = self.client.get('/api/v1/stateflow/states/?limit=10&offset=0') response = self.client.get('/api/v1/stateflow/states/?limit=10&offset=0')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 2) self.assertEqual(response.data['count'], 2)
@@ -52,43 +59,37 @@ class StateAPITestCase(TestCase):
def test_retrieve_state(self): def test_retrieve_state(self):
"""测试获取状态详情""" """测试获取状态详情"""
state = models.State.objects.create(name='测试状态', description='描述') self.state1.parameters.add(self.param1)
models.StateParameter.objects.create(state=state, key='key1', value='value1')
response = self.client.get(f'/api/v1/stateflow/states/{state.id}/') response = self.client.get(f'/api/v1/stateflow/states/{self.state1.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], '测试状态') self.assertEqual(response.data['name'], '状态1')
self.assertEqual(len(response.data['parameters']), 1) self.assertEqual(len(response.data['parameters']), 1)
def test_update_state(self): def test_update_state(self):
"""测试更新状态""" """测试更新状态"""
state = models.State.objects.create(name='旧名称', description='旧描述')
data = { data = {
'name': '新名称', 'name': '新名称',
'description': '新描述', 'description': '新描述',
'parameters': [ 'parameter_ids': [self.param1.id]
{'key': 'new_param', 'value': 'new_value', 'description': '新参数'},
]
} }
response = self.client.put(f'/api/v1/stateflow/states/{state.id}/', data, format='json') response = self.client.put(f'/api/v1/stateflow/states/{self.state1.id}/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
state.refresh_from_db() self.state1.refresh_from_db()
self.assertEqual(state.name, '新名称') self.assertEqual(self.state1.name, '新名称')
self.assertEqual(state.parameters.count(), 1) self.assertEqual(self.state1.parameters.count(), 1)
def test_delete_state(self): def test_delete_state(self):
"""测试删除状态""" """测试删除状态"""
state = models.State.objects.create(name='待删除状态') response = self.client.delete(f'/api/v1/stateflow/states/{self.state2.id}/')
response = self.client.delete(f'/api/v1/stateflow/states/{state.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(models.State.objects.filter(id=state.id).exists()) self.assertFalse(models.State.objects.filter(id=self.state2.id).exists())
def test_search_states(self): def test_search_states(self):
"""测试搜索状态""" """测试搜索状态"""
# 创建特定名称的状态用于搜索测试
models.State.objects.create(name='审核状态', description='需要审核') models.State.objects.create(name='审核状态', description='需要审核')
models.State.objects.create(name='发货状态', description='已经发货') models.State.objects.create(name='发货状态', description='已经发货')
@@ -105,18 +106,18 @@ class StateAPITestCase(TestCase):
content_type="text/plain" content_type="text/plain"
) )
# 先创建状态
state = models.State.objects.create(name='测试状态', description='带附件')
# 创建带附件的参数 # 创建带附件的参数
param = models.StateParameter.objects.create( param = models.StateParameter.objects.create(
state=state,
key='document', key='document',
value='测试文档', value='测试文档',
attachment=test_file, attachment=test_file,
description='这是一个测试文档' description='这是一个测试文档'
) )
# 创建状态并关联参数
state = models.State.objects.create(name='测试状态', description='带附件')
state.parameters.add(param)
# 验证附件已保存 # 验证附件已保存
self.assertIsNotNone(param.attachment) self.assertIsNotNone(param.attachment)
self.assertIn('test_doc', param.attachment.name) # 文件名可能有哈希前缀 self.assertIn('test_doc', param.attachment.name) # 文件名可能有哈希前缀
@@ -138,13 +139,13 @@ class StateAPITestCase(TestCase):
def test_state_parameter_without_attachment(self): def test_state_parameter_without_attachment(self):
"""测试创建不带附件的状态参数""" """测试创建不带附件的状态参数"""
state = models.State.objects.create(name='测试状态2', description='不带附件')
param = models.StateParameter.objects.create( param = models.StateParameter.objects.create(
state=state,
key='simple_param', key='simple_param',
value='简单值', value='简单值',
description='简单参数' description='简单参数'
) )
state = models.State.objects.create(name='测试状态2', description='不带附件')
state.parameters.add(param)
# 验证附件字段为空 # 验证附件字段为空
self.assertFalse(param.attachment) self.assertFalse(param.attachment)
@@ -161,14 +162,14 @@ class StateAPITestCase(TestCase):
def test_update_state_parameter_with_attachment(self): def test_update_state_parameter_with_attachment(self):
"""测试更新状态参数时添加附件""" """测试更新状态参数时添加附件"""
# 创建初始状态和参数(不带附件) # 创建初始参数(不带附件)
state = models.State.objects.create(name='测试状态3', description='更新附件')
param = models.StateParameter.objects.create( param = models.StateParameter.objects.create(
state=state,
key='updatable_param', key='updatable_param',
value='初始值', value='初始值',
description='可更新参数' description='可更新参数'
) )
state = models.State.objects.create(name='测试状态3', description='更新附件')
state.parameters.add(param)
# 验证初始无附件 # 验证初始无附件
self.assertFalse(param.attachment) self.assertFalse(param.attachment)
@@ -206,13 +207,13 @@ class StateAPITestCase(TestCase):
content_type="image/jpeg" content_type="image/jpeg"
) )
state = models.State.objects.create(name='图片状态', description='带图片')
param = models.StateParameter.objects.create( param = models.StateParameter.objects.create(
state=state,
key='image', key='image',
value='测试图片', value='测试图片',
attachment=test_file attachment=test_file
) )
state = models.State.objects.create(name='图片状态', description='带图片')
state.parameters.add(param)
# 通过 API 获取 # 通过 API 获取
response = self.client.get(f'/api/v1/stateflow/states/{state.id}/') response = self.client.get(f'/api/v1/stateflow/states/{state.id}/')
@@ -229,6 +230,129 @@ class StateAPITestCase(TestCase):
# 清理 # 清理
if param.attachment: if param.attachment:
param.attachment.delete() param.attachment.delete()
def test_state_parameter_is_required(self):
"""测试参数必填字段"""
# 创建必填参数
required_param = models.StateParameter.objects.create(
key='required_param',
value='必填参数',
is_required=True
)
# 创建可选参数
optional_param = models.StateParameter.objects.create(
key='optional_param',
value='可选参数',
is_required=False
)
# 创建状态并关联参数
state = models.State.objects.create(name='测试状态', description='测试必填参数')
state.parameters.add(required_param, optional_param)
# 通过 API 验证
response = self.client.get(f'/api/v1/stateflow/states/{state.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
parameters = response.data['parameters']
self.assertEqual(len(parameters), 2)
# 验证必填字段正确返回
required_params = [p for p in parameters if p['is_required']]
optional_params = [p for p in parameters if not p['is_required']]
self.assertEqual(len(required_params), 1)
self.assertEqual(len(optional_params), 1)
self.assertEqual(required_params[0]['key'], 'required_param')
def test_state_get_parameters_method(self):
"""测试 State.get_parameters() 方法"""
# 创建必填和可选参数
required_param = models.StateParameter.objects.create(
key='required1',
value='必填',
is_required=True
)
optional_param = models.StateParameter.objects.create(
key='optional1',
value='可选',
is_required=False
)
state = models.State.objects.create(name='测试状态')
state.parameters.add(required_param, optional_param)
# 测试获取所有参数
all_params = state.get_parameters(required_only=False)
self.assertEqual(len(all_params), 2)
# 测试只获取必填参数
required_params = state.get_parameters(required_only=True)
self.assertEqual(len(required_params), 1)
self.assertEqual(required_params[0].key, 'required1')
self.assertTrue(required_params[0].is_required)
def test_state_parameter_is_image_path(self):
"""测试参数图片路径字段"""
# 创建普通参数
normal_param = models.StateParameter.objects.create(
key='normal_param',
value='普通文本值',
is_image_path=False
)
# 创建图片路径参数
image_param = models.StateParameter.objects.create(
key='image_param',
value='https://example.com/image.jpg',
is_image_path=True
)
# 创建状态并关联参数
state = models.State.objects.create(name='测试图片参数', description='测试is_image_path字段')
state.parameters.add(normal_param, image_param)
# 通过 API 验证
response = self.client.get(f'/api/v1/stateflow/states/{state.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
parameters = response.data['parameters']
self.assertEqual(len(parameters), 2)
# 验证 is_image_path 字段正确返回
image_params = [p for p in parameters if p['is_image_path']]
normal_params = [p for p in parameters if not p['is_image_path']]
self.assertEqual(len(image_params), 1)
self.assertEqual(len(normal_params), 1)
self.assertEqual(image_params[0]['key'], 'image_param')
self.assertEqual(image_params[0]['value'], 'https://example.com/image.jpg')
def test_create_parameter_with_image_path_flag(self):
"""测试创建带图片路径标记的参数"""
param = models.StateParameter.objects.create(
key='product_image',
value='https://cdn.example.com/products/abc123.png',
is_image_path=True,
description='产品图片URL'
)
# 验证字段已保存
self.assertTrue(param.is_image_path)
self.assertIn('https://', param.value)
# 创建状态并关联
state = models.State.objects.create(name='产品展示')
state.parameters.add(param)
# API 验证
response = self.client.get(f'/api/v1/stateflow/states/{state.id}/')
params = response.data['parameters']
self.assertEqual(len(params), 1)
self.assertTrue(params[0]['is_image_path'])
self.assertEqual(params[0]['key'], 'product_image')
class ProcessAPITestCase(TestCase): class ProcessAPITestCase(TestCase):
@@ -322,3 +446,189 @@ class ProcessAPITestCase(TestCase):
response = self.client.get('/api/v1/stateflow/processes/?search=订单&limit=10&offset=0') response = self.client.get('/api/v1/stateflow/processes/?search=订单&limit=10&offset=0')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['count'], 1)
class BusinessObjectNewAPITestCase(TestCase):
"""测试 BusinessObject 新增的 API"""
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='testuser', password='testpass')
self.client.force_authenticate(user=self.user)
# 创建状态和参数
self.state1 = models.State.objects.create(name='状态1', description='第一个状态')
self.state2 = models.State.objects.create(name='状态2', description='第二个状态')
self.state3 = models.State.objects.create(name='状态3', description='第三个状态')
# 为状态1添加参数
self.param1 = models.StateParameter.objects.create(
key='param1', value='value1', is_required=True, description='必填参数'
)
self.param2 = models.StateParameter.objects.create(
key='param2', value='value2', is_required=False, description='可选参数'
)
self.state1.parameters.add(self.param1, self.param2)
# 创建流程
self.process = models.Process.objects.create(name='测试流程')
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建业务对象
self.business_object = models.BusinessObject.objects.create(
name='测试业务对象',
process=self.process
)
def test_next_pending_state_api(self):
"""测试获取下一个待执行节点 API"""
# 未开始时应返回第一个节点
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/next_pending_state/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['state']['id'], self.state1.id)
self.assertEqual(response.data['order'], 0)
self.assertIn('parameters', response.data)
self.assertEqual(len(response.data['parameters']), 2)
# 测试不包含参数
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/next_pending_state/?include_parameters=false'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('parameters', response.data)
# 推进一步(提供必填参数)
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'param1': 'test_value'}},
format='json'
)
# 应返回第二个节点
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/next_pending_state/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['state']['id'], self.state2.id)
# 完成所有节点
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
# 应返回 None
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/next_pending_state/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIsNone(response.data['next_state'])
def test_pending_states_api(self):
"""测试获取所有待执行节点 API"""
# 初始状态应返回所有节点
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/pending_states/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 3)
self.assertEqual(len(response.data['pending_states']), 3)
# 验证不包含 parameters
self.assertNotIn('parameters', response.data['pending_states'][0])
# 推进一步(提供必填参数)
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'param1': 'test_value'}},
format='json'
)
# 应只剩两个待执行节点
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/pending_states/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 2)
# 完成所有节点
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
# 应没有待执行节点
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/pending_states/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 0)
def test_current_state_parameters_api(self):
"""测试获取当前状态(最后完成状态)参数 API"""
# 未开始时current_state 为 None应返回无参数消息
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/current_state_parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['parameters']), 0)
self.assertIn('message', response.data)
# 推进完成第一个状态current_state 变为 state1最后完成的状态
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'param1': 'test_value'}},
format='json'
)
# 现在 current_state 是 state1有参数
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/current_state_parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['state']['id'], self.state1.id)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['parameters']), 2)
# 测试只获取必填参数
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/current_state_parameters/?required_only=true'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
self.assertEqual(response.data['parameters'][0]['key'], 'param1')
# 推进完成第二个状态current_state 变为 state2无参数
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/current_state_parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['state']['id'], self.state2.id)
self.assertEqual(response.data['count'], 0)
def test_state_parameters_api(self):
"""测试 State 的参数 API"""
# 获取状态1的所有参数
response = self.client.get(
f'/api/v1/stateflow/states/{self.state1.id}/parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['state_id'], self.state1.id)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['parameters']), 2)
# 只获取必填参数
response = self.client.get(
f'/api/v1/stateflow/states/{self.state1.id}/parameters/?required_only=true'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
# 测试没有参数的状态
response = self.client.get(
f'/api/v1/stateflow/states/{self.state2.id}/parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 0)

View File

@@ -1,158 +0,0 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from . import models, services
User = get_user_model()
class OrderStateFlowTestCase(TestCase):
"""测试订单状态流转逻辑"""
def setUp(self):
"""设置测试数据"""
# 创建测试用户
self.user = User.objects.create_user(username='testuser', password='testpass')
# 创建状态节点
self.state1 = models.State.objects.create(name='状态1', description='第一个状态')
self.state2 = models.State.objects.create(name='状态2', description='第二个状态')
self.state3 = models.State.objects.create(name='状态3', description='第三个状态')
# 创建流程
self.process = models.Process.objects.create(name='测试流程', description='用于测试的流程')
# 添加流程节点
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建订单(业务对象)
from django.contrib.contenttypes.models import ContentType
# 使用 Process 作为临时的关联对象(实际使用时应该关联真实的业务对象)
ct = ContentType.objects.get_for_model(models.Process)
self.business_object = models.BusinessObject.objects.create(
name='测试订单',
process=self.process,
description='测试订单描述',
content_type=ct,
object_id=self.process.id
)
def test_initial_state(self):
"""测试初始状态 - 应该是 None未开始"""
current_state = self.business_object.get_current_state()
self.assertIsNone(current_state, '初始状态应该是 None未开始')
self.assertEqual(self.business_object.get_progress_percentage(), 0.0)
# 整体状态应该是 not_started
status = services.get_overall_status(self.business_object)
self.assertEqual(status, 'not_started')
def test_advance_to_next_state(self):
"""测试推进到下一个状态"""
# 第一次推进应该完成第一个状态
success, message = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success)
current_state = self.business_object.get_current_state()
self.assertEqual(current_state.id, self.state2.id)
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 33.33, places=1)
# 推进到状态3
success, message = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success)
current_state = self.business_object.get_current_state()
self.assertEqual(current_state.id, self.state3.id)
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 66.67, places=1)
# 完成最后一个状态
success, message = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success)
self.assertEqual(self.business_object.get_progress_percentage(), 100.0)
# 应该返回 None所有状态都已完成
current_state = self.business_object.get_current_state()
self.assertIsNone(current_state)
# 整体状态应该是 completed
status = services.get_overall_status(self.business_object)
self.assertEqual(status, 'completed')
def test_state_status(self):
"""测试状态的状态"""
# 初始状态,所有状态都应该是 not_started
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state1), 'not_started')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state2), 'not_started')
# 完成第一个状态
services.advance_to_next_state(self.business_object, self.user)
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state1), 'completed')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state2), 'in_progress')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state3), 'not_started')
def test_timeline(self):
"""测试时间线"""
timeline = self.business_object.get_timeline()
self.assertEqual(len(timeline), 3)
# 所有状态都应该是 not_started
for item in timeline:
self.assertEqual(item['status'], 'not_started')
# 完成第一个状态
services.advance_to_next_state(self.business_object, self.user)
timeline = self.business_object.get_timeline()
self.assertEqual(timeline[0]['status'], 'completed')
self.assertEqual(timeline[1]['status'], 'in_progress')
self.assertEqual(timeline[2]['status'], 'not_started')
self.assertIsNotNone(timeline[0]['completed_by'])
self.assertEqual(timeline[0]['completed_by'].id, self.user.id)
self.assertFalse(timeline[0]['is_cancelled'])
def test_reset_progress(self):
"""测试重置进度 - 不删除记录,而是标记为已撤销"""
# 完成两个状态
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
# 确认有2条日志记录
self.assertEqual(self.business_object.state_logs.count(), 2)
# 重置进度
services.reset_business_object_progress(self.business_object)
# 日志记录应该还在(不删除)
self.assertEqual(self.business_object.state_logs.count(), 2)
# 但所有记录都应该标记为已撤销
cancelled_count = self.business_object.state_logs.filter(is_cancelled=True).count()
self.assertEqual(cancelled_count, 2)
# 应该回到初始状态(未开始)
current_state = self.business_object.get_current_state()
self.assertIsNone(current_state)
# 所有撤销的记录都应该有撤销时间
for log in self.business_object.state_logs.all():
self.assertTrue(log.is_cancelled)
self.assertIsNotNone(log.cancelled_at)
def test_timeline_with_cancelled(self):
"""测试包含撤销记录的时间线"""
# 推进并完成所有状态
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
# 重置进度
services.reset_business_object_progress(self.business_object)
timeline = self.business_object.get_timeline()
# 所有状态都应该显示为 cancelled
for item in timeline:
self.assertEqual(item['status'], 'cancelled')
self.assertTrue(item['is_cancelled'])
self.assertIsNotNone(item['cancelled_at'])

View File

@@ -0,0 +1,471 @@
"""
测试 BusinessObject 的完整 API 端点
包括 reset, timeline, add_parameters_to_log, get_log_parameters 等
"""
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from stateflow import models, services
User = get_user_model()
class BusinessObjectAPITestCase(TestCase):
"""测试 BusinessObject 的所有 API 端点"""
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='testuser', password='testpass')
self.client.force_authenticate(user=self.user)
# 创建状态
self.state1 = models.State.objects.create(name='状态1', description='第一个状态')
self.state2 = models.State.objects.create(name='状态2', description='第二个状态')
self.state3 = models.State.objects.create(name='状态3', description='第三个状态')
# 为状态添加参数
self.param1 = models.StateParameter.objects.create(
key='temperature',
value='',
is_required=True,
description='温度'
)
self.param2 = models.StateParameter.objects.create(
key='humidity',
value='',
is_required=False,
description='湿度'
)
self.state1.parameters.add(self.param1, self.param2)
# 创建流程
self.process = models.Process.objects.create(name='测试流程')
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建业务对象
self.business_object = models.BusinessObject.objects.create(
name='测试业务对象',
process=self.process
)
def test_reset_api(self):
"""测试重置进度 API"""
# 先推进两次
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
# 验证已有进度
self.business_object.refresh_from_db()
self.assertIsNotNone(services.get_business_object_current_state(self.business_object))
self.assertEqual(self.business_object.state_logs.count(), 2)
# 重置进度
response = self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/reset/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertIn('重置', response.data['message'])
# 验证进度已重置
self.business_object.refresh_from_db()
self.assertIsNone(services.get_business_object_current_state(self.business_object))
self.assertEqual(self.business_object.state_logs.filter(is_cancelled=False).count(), 0)
def test_reset_api_not_started(self):
"""测试重置未开始的业务对象"""
# 未开始也可以重置(不会报错)
response = self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/reset/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
def test_timeline_api(self):
"""测试状态时间线 API"""
# 未开始时的时间线
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 3)
# 验证所有状态都是待执行
for item in response.data:
self.assertEqual(item['status'], 'not_started')
self.assertIsNone(item['completed_at'])
self.assertIsNone(item['completed_by'])
self.assertFalse(item['is_cancelled'])
# 推进第一个状态
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
# 再次获取时间线
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 3)
# 第一个状态应该是已完成
self.assertEqual(response.data[0]['status'], 'completed')
self.assertIsNotNone(response.data[0]['completed_at'])
self.assertEqual(response.data[0]['completed_by'], 'testuser')
self.assertFalse(response.data[0]['is_cancelled'])
# 其他状态仍是待执行
self.assertEqual(response.data[1]['status'], 'not_started')
self.assertEqual(response.data[2]['status'], 'not_started')
def test_timeline_api_with_step_back(self):
"""测试时间线 API 包含回退后的已撤销状态"""
# 推进两次
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
# 回退一次
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
# 获取时间线
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# 第一个状态:已完成且未撤销
self.assertEqual(response.data[0]['status'], 'completed')
self.assertFalse(response.data[0]['is_cancelled'])
# 第二个状态:已撤销
self.assertEqual(response.data[1]['status'], 'cancelled')
self.assertTrue(response.data[1]['is_cancelled'])
self.assertIsNotNone(response.data[1]['cancelled_at'])
# 第三个状态:待执行
self.assertEqual(response.data[2]['status'], 'not_started')
def test_add_parameters_to_log_api(self):
"""测试为状态流转记录补充参数 API"""
# 先推进一次
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 补充参数
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
{
'parameters': {
'humidity': '60%',
'operator': '张三'
},
'remark': '补充测量数据'
},
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertIn('parameter_record', response.data)
# 验证参数记录
param_record = response.data['parameter_record']
self.assertEqual(param_record['parameters']['humidity'], '60%')
self.assertEqual(param_record['parameters']['operator'], '张三')
self.assertEqual(param_record['remark'], '补充测量数据')
def test_add_parameters_to_log_api_empty_parameters(self):
"""测试补充空参数应该失败"""
# 先推进一次
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 尝试补充空参数
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
{
'parameters': {},
'remark': '空参数测试'
},
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertIn('不能为空', response.data['message'])
def test_add_parameters_to_log_api_nonexistent_log(self):
"""测试为不存在的记录补充参数应该失败"""
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/99999/add-parameters/',
{
'parameters': {'test': 'value'},
'remark': '测试'
},
format='json'
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertFalse(response.data['success'])
self.assertIn('不存在', response.data['message'])
def test_get_log_parameters_api_all_records(self):
"""测试获取状态流转记录的所有参数记录 API"""
# 推进并提供初始参数
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5', 'humidity': '60%'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 补充参数两次
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
{
'parameters': {'temperature': '26.0'},
'remark': '第一次重测'
},
format='json'
)
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
{
'parameters': {'temperature': '26.5'},
'remark': '第二次重测'
},
format='json'
)
# 获取所有参数记录
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['state_log_id'], state_log_id)
self.assertFalse(response.data['is_cancelled'])
self.assertEqual(response.data['count'], 3)
# 验证汇总数据
summary = response.data['summary']
self.assertEqual(summary['temperature'], '26.5') # 最新值
self.assertEqual(summary['humidity'], '60%')
# 验证记录列表
self.assertEqual(len(response.data['records']), 3)
def test_get_log_parameters_api_by_key(self):
"""测试获取指定参数的历史记录 API"""
# 推进并提供初始参数
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 补充参数
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
{
'parameters': {'temperature': '26.0', 'humidity': '65%'},
'remark': '重测'
},
format='json'
)
# 获取 temperature 的历史
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?key=temperature'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['key'], 'temperature')
self.assertEqual(len(response.data['history']), 2)
self.assertEqual(response.data['history'][0]['value'], '25.5')
self.assertEqual(response.data['history'][1]['value'], '26.0')
self.assertEqual(response.data['history'][1]['remark'], '重测')
def test_get_log_parameters_api_cancelled_state_excluded(self):
"""测试已撤销状态的参数默认不可见"""
# 推进
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 回退(撤销状态)
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
# 默认获取参数(应该返回空)
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['is_cancelled'])
self.assertEqual(len(response.data['summary']), 0) # 汇总应该为空
self.assertEqual(len(response.data['records']), 0) # 记录列表应该为空
def test_get_log_parameters_api_cancelled_state_with_include_flag(self):
"""测试 include_cancelled=true 可以查看已撤销状态的参数"""
# 推进
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5', 'humidity': '60%'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 回退(撤销状态)
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
# 使用 include_cancelled=true 获取参数
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?include_cancelled=true'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['is_cancelled'])
self.assertEqual(len(response.data['summary']), 2) # 应该能看到参数
self.assertEqual(response.data['summary']['temperature'], '25.5')
self.assertEqual(response.data['summary']['humidity'], '60%')
def test_get_log_parameters_api_key_history_cancelled_excluded(self):
"""测试已撤销状态的参数历史默认不可见"""
# 推进
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
state_log_id = response.data['state_log']['id']
# 补充参数
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/add-parameters/',
{'parameters': {'temperature': '26.0'}, 'remark': '重测'},
format='json'
)
# 回退
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/step_back/')
# 获取参数历史(默认应该为空)
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?key=temperature'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['history']), 0)
# 使用 include_cancelled=true 查看
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/{state_log_id}/parameters/?key=temperature&include_cancelled=true'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['history']), 2)
def test_get_log_parameters_api_nonexistent_log(self):
"""测试获取不存在的记录的参数应该返回 404"""
response = self.client.get(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/state-logs/99999/parameters/'
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertFalse(response.data['success'])
def test_advance_api_with_parameters(self):
"""测试推进 API 携带参数"""
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{
'parameters': {
'temperature': '25.5',
'humidity': '60%',
'operator': '张三'
}
},
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['success'])
self.assertIn('state_log', response.data)
# 验证参数摘要API 返回的是 parameters_summary不是 parameter_records
state_log = response.data['state_log']
self.assertIn('parameters_summary', state_log)
params = state_log['parameters_summary']
self.assertEqual(params['temperature'], '25.5')
self.assertEqual(params['humidity'], '60%')
self.assertEqual(params['operator'], '张三')
def test_advance_api_missing_required_parameter(self):
"""测试推进 API 缺少必填参数应该失败"""
response = self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{
'parameters': {
'humidity': '60%' # 只提供可选参数,缺少必填的 temperature
}
},
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(response.data['success'])
self.assertIn('缺失必填参数', response.data['message'])
self.assertIn('temperature', response.data['message'])
def test_timeline_completed_all_states(self):
"""测试完成所有状态后的时间线"""
# 完成所有状态
self.client.post(
f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/',
{'parameters': {'temperature': '25.5'}},
format='json'
)
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
self.client.post(f'/api/v1/stateflow/business-objects/{self.business_object.id}/advance/')
# 获取时间线
response = self.client.get(f'/api/v1/stateflow/business-objects/{self.business_object.id}/timeline/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# 所有状态都应该是已完成
for item in response.data:
self.assertEqual(item['status'], 'completed')
self.assertIsNotNone(item['completed_at'])
self.assertIsNotNone(item['completed_by'])
self.assertFalse(item['is_cancelled'])

View File

@@ -0,0 +1,374 @@
"""
测试参数管理功能
包括必填参数验证、参数记录创建、回退后参数可见性等
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from stateflow import models, services
User = get_user_model()
class ParameterManagementTestCase(TestCase):
"""测试参数管理功能"""
def setUp(self):
"""设置测试数据"""
# 创建测试用户
self.user = User.objects.create_user(username='testuser', password='testpass')
# 创建状态
self.state1 = models.State.objects.create(name='状态1', description='第一个状态')
self.state2 = models.State.objects.create(name='状态2', description='第二个状态')
self.state3 = models.State.objects.create(name='状态3', description='第三个状态')
# 为状态1添加必填参数
self.required_param = models.StateParameter.objects.create(
key='temperature',
value='',
is_required=True,
description='温度(必填)'
)
self.optional_param = models.StateParameter.objects.create(
key='humidity',
value='',
is_required=False,
description='湿度(可选)'
)
self.state1.parameters.add(self.required_param, self.optional_param)
# 为状态2添加必填参数
self.required_param2 = models.StateParameter.objects.create(
key='pressure',
value='',
is_required=True,
description='压力(必填)'
)
self.state2.parameters.add(self.required_param2)
# 创建流程
self.process = models.Process.objects.create(name='测试流程')
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建业务对象
self.business_object = models.BusinessObject.objects.create(
name='测试业务对象',
process=self.process
)
def test_advance_without_required_parameter_fails(self):
"""测试不提供必填参数时推进失败"""
# 尝试推进但不提供必填参数
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user
)
# 应该失败
self.assertFalse(success)
self.assertIn('缺失必填参数', message)
self.assertIn('temperature', message)
self.assertIsNone(state_log)
# 验证状态未改变
current_state = services.get_business_object_current_state(self.business_object)
self.assertIsNone(current_state)
# 验证没有创建状态流转记录
self.assertEqual(self.business_object.state_logs.count(), 0)
def test_advance_with_only_required_parameter_succeeds(self):
"""测试只提供必填参数时推进成功"""
# 只提供必填参数
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5' # 只提供必填的
)
# 应该成功
self.assertTrue(success)
self.assertIsNotNone(state_log)
# 验证状态已推进
current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.name, self.state1.name)
# 验证参数记录已创建
self.assertEqual(state_log.parameter_records.count(), 1)
param_record = state_log.parameter_records.first()
self.assertEqual(param_record.parameters['temperature'], '25.5')
self.assertNotIn('humidity', param_record.parameters)
def test_advance_with_all_parameters(self):
"""测试提供所有参数时推进成功"""
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5',
humidity='60%'
)
# 应该成功
self.assertTrue(success)
self.assertIsNotNone(state_log)
# 验证参数记录包含所有提供的参数
param_record = state_log.parameter_records.first()
self.assertEqual(param_record.parameters['temperature'], '25.5')
self.assertEqual(param_record.parameters['humidity'], '60%')
def test_advance_with_extra_parameters(self):
"""测试提供额外参数时也能成功(不做验证)"""
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5',
humidity='60%',
operator='张三', # 额外参数
notes='测试备注' # 额外参数
)
# 应该成功(不验证参数是否在定义中)
self.assertTrue(success)
# 验证所有参数都被保存
param_record = state_log.parameter_records.first()
self.assertEqual(len(param_record.parameters), 4)
self.assertEqual(param_record.parameters['operator'], '张三')
self.assertEqual(param_record.parameters['notes'], '测试备注')
def test_add_parameters_after_advance(self):
"""测试推进后补充参数"""
# 先推进
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5'
)
self.assertTrue(success)
# 补充参数
new_record = services.add_parameters_to_state_log(
state_log,
remark='补充测量',
humidity='65%',
inspector='李四'
)
# 验证创建了新的参数记录
self.assertEqual(state_log.parameter_records.count(), 2)
self.assertEqual(new_record.parameters['humidity'], '65%')
self.assertEqual(new_record.parameters['inspector'], '李四')
self.assertEqual(new_record.remark, '补充测量')
def test_parameter_summary_with_updates(self):
"""测试参数汇总(包含更新的值)"""
# 推进并提供初始参数
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5',
humidity='60%'
)
# 补充参数(更新 temperature
services.add_parameters_to_state_log(
state_log,
temperature='26.0', # 重新测量
remark='温度重新测量'
)
# 获取参数汇总
summary = state_log.get_all_parameters_summary()
# 验证最新值
self.assertEqual(summary['temperature'], '26.0') # 应该是更新后的值
self.assertEqual(summary['humidity'], '60%') # 保持原值
def test_parameter_history(self):
"""测试参数历史记录"""
# 推进
success, message, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5'
)
# 多次更新同一个参数
services.add_parameters_to_state_log(
state_log,
temperature='26.0',
remark='第一次重测'
)
services.add_parameters_to_state_log(
state_log,
temperature='26.5',
remark='第二次重测'
)
# 获取历史
history = state_log.get_parameter_history('temperature')
# 验证历史记录
self.assertEqual(len(history), 3)
self.assertEqual(history[0]['value'], '25.5')
self.assertEqual(history[1]['value'], '26.0')
self.assertEqual(history[1]['remark'], '第一次重测')
self.assertEqual(history[2]['value'], '26.5')
self.assertEqual(history[2]['remark'], '第二次重测')
def test_step_back_excludes_cancelled_state_parameters(self):
"""测试回退后,默认情况下查询参数时应排除已撤销状态的参数"""
# 推进两次,每次都提供参数
success, msg, state_log1 = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5',
humidity='60%'
)
self.assertTrue(success)
success, msg, state_log2 = services.advance_to_next_state(
self.business_object,
self.user,
pressure='1013'
)
self.assertTrue(success)
# 验证两个状态都有参数记录
self.assertEqual(state_log1.parameter_records.count(), 1)
self.assertEqual(state_log2.parameter_records.count(), 1)
# 回退一步(撤销 state2
success, message = services.step_back_one_state(self.business_object, self.user)
self.assertTrue(success)
# 刷新 state_log2
state_log2.refresh_from_db()
self.assertTrue(state_log2.is_cancelled)
# 默认情况下,已撤销状态的参数不可见
summary = state_log2.get_all_parameters_summary()
self.assertEqual(len(summary), 0) # 应该返回空字典
# 使用 include_cancelled=True 可以查看已撤销状态的参数
summary_with_cancelled = state_log2.get_all_parameters_summary(include_cancelled=True)
self.assertEqual(len(summary_with_cancelled), 1)
self.assertEqual(summary_with_cancelled['pressure'], '1013')
# 验证 state_log1 的参数仍然可见(未撤销)
summary1 = state_log1.get_all_parameters_summary()
self.assertEqual(len(summary1), 2)
self.assertEqual(summary1['temperature'], '25.5')
self.assertEqual(summary1['humidity'], '60%')
def test_cancelled_state_parameter_history(self):
"""测试已撤销状态的参数历史记录不可见"""
# 推进并提供参数
success, msg, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5'
)
self.assertTrue(success)
# 补充参数
services.add_parameters_to_state_log(
state_log,
temperature='26.0',
remark='重新测量'
)
# 验证参数历史存在
history = state_log.get_parameter_history('temperature')
self.assertEqual(len(history), 2)
# 回退(撤销状态)
services.step_back_one_state(self.business_object, self.user)
state_log.refresh_from_db()
# 默认情况下,已撤销状态的参数历史不可见
history = state_log.get_parameter_history('temperature')
self.assertEqual(len(history), 0)
# 使用 include_cancelled=True 可以查看
history_with_cancelled = state_log.get_parameter_history('temperature', include_cancelled=True)
self.assertEqual(len(history_with_cancelled), 2)
def test_cancelled_state_get_latest_parameter(self):
"""测试已撤销状态无法获取最新参数值"""
# 推进并提供参数
success, msg, state_log = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5'
)
self.assertTrue(success)
# 验证可以获取参数
temp = state_log.get_latest_parameter('temperature')
self.assertEqual(temp, '25.5')
# 回退(撤销状态)
services.step_back_one_state(self.business_object, self.user)
state_log.refresh_from_db()
# 默认情况下返回默认值
temp = state_log.get_latest_parameter('temperature', default='N/A')
self.assertEqual(temp, 'N/A')
# 使用 include_cancelled=True 可以获取
temp_with_cancelled = state_log.get_latest_parameter('temperature', include_cancelled=True)
self.assertEqual(temp_with_cancelled, '25.5')
def test_multiple_states_with_different_required_parameters(self):
"""测试多个状态各自的必填参数验证"""
# 推进到 state1需要 temperature
success, msg, _ = services.advance_to_next_state(
self.business_object,
self.user,
temperature='25.5'
)
self.assertTrue(success)
# 尝试推进到 state2 但不提供 pressure
success, msg, _ = services.advance_to_next_state(
self.business_object,
self.user,
temperature='26.0' # 提供了 state1 的参数,但这是 state2
)
# 应该失败(缺少 state2 的必填参数 pressure
self.assertFalse(success)
self.assertIn('缺失必填参数', msg)
self.assertIn('pressure', msg)
# 提供正确的必填参数
success, msg, _ = services.advance_to_next_state(
self.business_object,
self.user,
pressure='1013'
)
self.assertTrue(success)
def test_state_without_required_parameters_allows_advance_without_params(self):
"""测试没有必填参数的状态允许不提供参数推进"""
# 先推进两次到达 state3没有任何参数要求
services.advance_to_next_state(self.business_object, self.user, temperature='25')
services.advance_to_next_state(self.business_object, self.user, pressure='1013')
# 推进到 state3不提供任何参数
success, msg, state_log = services.advance_to_next_state(
self.business_object,
self.user
)
# 应该成功
self.assertTrue(success)
self.assertIsNotNone(state_log)
# 验证没有创建参数记录
self.assertEqual(state_log.parameter_records.count(), 0)

View File

@@ -0,0 +1,288 @@
"""
Stateflow 服务层单元测试
测试新增的服务函数以及 current_state 语义修正
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from stateflow import models, services
User = get_user_model()
class StateFlowServicesTestCase(TestCase):
"""测试订单状态流转逻辑和新增服务函数"""
def setUp(self):
"""设置测试数据"""
# 创建测试用户
self.user = User.objects.create_user(username='testuser', password='testpass')
# 创建状态节点
self.state1 = models.State.objects.create(name='状态1', description='第一个状态')
self.state2 = models.State.objects.create(name='状态2', description='第二个状态')
self.state3 = models.State.objects.create(name='状态3', description='第三个状态')
# 创建流程
self.process = models.Process.objects.create(name='测试流程', description='用于测试的流程')
# 添加流程节点
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建业务对象
ct = ContentType.objects.get_for_model(models.Process)
self.business_object = models.BusinessObject.objects.create(
name='测试订单',
process=self.process,
description='测试订单描述',
content_type=ct,
object_id=self.process.id
)
def test_initial_state(self):
"""测试初始状态 - 应该是 None未开始"""
current_state = self.business_object.get_current_state()
self.assertIsNone(current_state, '初始状态应该是 None未开始')
self.assertEqual(self.business_object.get_progress_percentage(), 0.0)
# 整体状态应该是 not_started
status = services.get_overall_status(self.business_object)
self.assertEqual(status, 'not_started')
def test_advance_to_next_state(self):
"""测试推进到下一个状态"""
# 第一次推进应该完成第一个状态
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success)
# current_state 应该是最后完成的状态state1
current_state = self.business_object.get_current_state()
self.assertEqual(current_state.name, self.state1.name)
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 33.33, places=1)
# 推进到状态2完成state2
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success)
# current_state 应该是 state2
current_state = self.business_object.get_current_state()
self.assertEqual(current_state.name, self.state2.name)
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 66.67, places=1)
# 完成最后一个状态state3
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success)
self.assertEqual(self.business_object.get_progress_percentage(), 100.0)
# current_state 应该是 state3最后完成的状态
current_state = self.business_object.get_current_state()
self.assertEqual(current_state.name, self.state3.name)
# 整体状态应该是 completed
status = services.get_overall_status(self.business_object)
self.assertEqual(status, 'completed')
def test_state_status(self):
"""测试状态的状态"""
# 初始状态,所有状态都应该是 not_started
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state1), 'not_started')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state2), 'not_started')
# 完成第一个状态
services.advance_to_next_state(self.business_object, self.user)
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state1), 'completed')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state2), 'not_started')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state3), 'not_started')
# 完成第二个状态
services.advance_to_next_state(self.business_object, self.user)
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state1), 'completed')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state2), 'completed')
self.assertEqual(services.get_business_object_state_status(self.business_object, self.state3), 'not_started')
def test_timeline(self):
"""测试时间线"""
timeline = self.business_object.get_timeline()
self.assertEqual(len(timeline), 3)
# 所有状态都应该是 not_started
for item in timeline:
self.assertEqual(item['status'], 'not_started')
# 完成第一个状态
services.advance_to_next_state(self.business_object, self.user)
timeline = self.business_object.get_timeline()
self.assertEqual(timeline[0]['status'], 'completed')
self.assertEqual(timeline[1]['status'], 'not_started')
self.assertEqual(timeline[2]['status'], 'not_started')
self.assertIsNotNone(timeline[0]['completed_by'])
self.assertEqual(timeline[0]['completed_by'].id, self.user.id)
self.assertFalse(timeline[0]['is_cancelled'])
def test_reset_progress(self):
"""测试重置进度 - 不删除记录,而是标记为已撤销"""
# 完成两个状态
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
# 确认有2条日志记录
self.assertEqual(self.business_object.state_logs.count(), 2)
# 重置进度
services.reset_business_object_progress(self.business_object)
# 日志记录应该还在(不删除)
self.assertEqual(self.business_object.state_logs.count(), 2)
# 但所有记录都应该标记为已撤销
cancelled_count = self.business_object.state_logs.filter(is_cancelled=True).count()
self.assertEqual(cancelled_count, 2)
# 应该回到初始状态(未开始)
current_state = self.business_object.get_current_state()
self.assertIsNone(current_state)
# 所有撤销的记录都应该有撤销时间
for log in self.business_object.state_logs.all():
self.assertTrue(log.is_cancelled)
self.assertIsNotNone(log.cancelled_at)
def test_timeline_with_cancelled(self):
"""测试包含撤销记录的时间线"""
# 推进并完成所有状态
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
# 重置进度
services.reset_business_object_progress(self.business_object)
timeline = self.business_object.get_timeline()
# 所有状态都应该显示为 cancelled
for item in timeline:
self.assertEqual(item['status'], 'cancelled')
self.assertTrue(item['is_cancelled'])
self.assertIsNotNone(item['cancelled_at'])
def test_get_next_pending_state(self):
"""测试获取下一个待执行节点(新增功能)"""
# 创建参数
param1 = models.StateParameter.objects.create(
key='param1', value='value1', is_required=True
)
param2 = models.StateParameter.objects.create(
key='param2', value='value2', is_required=False
)
self.state1.parameters.add(param1, param2)
# 初始状态 - 应该返回第一个节点
next_state = services.get_next_pending_state(self.business_object)
self.assertIsNotNone(next_state)
self.assertEqual(next_state['state'].name, self.state1.name)
self.assertEqual(next_state['order'], 0)
self.assertIn('parameters', next_state)
self.assertEqual(len(next_state['parameters']), 2)
# 测试不包含参数
next_state_no_params = services.get_next_pending_state(
self.business_object, include_parameters=False
)
self.assertIsNotNone(next_state_no_params)
self.assertNotIn('parameters', next_state_no_params)
# 完成第一个状态(提供必填参数)
success, msg, _ = services.advance_to_next_state(
self.business_object,
self.user,
param1='value1' # 提供必填参数
)
self.assertTrue(success)
# 应该返回第二个节点
next_state = services.get_next_pending_state(self.business_object)
self.assertIsNotNone(next_state)
self.assertEqual(next_state['state'].name, self.state2.name)
self.assertEqual(next_state['order'], 1)
# 完成所有状态
services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user)
# 应该返回 None已完成
next_state = services.get_next_pending_state(self.business_object)
self.assertIsNone(next_state)
def test_get_all_pending_states(self):
"""测试获取所有待执行节点列表(新增功能)"""
# 初始状态 - 所有节点都待执行
pending_states = services.get_all_pending_states(self.business_object)
self.assertEqual(len(pending_states), 3)
self.assertEqual(pending_states[0]['state'].name, self.state1.name)
self.assertEqual(pending_states[1]['state'].name, self.state2.name)
self.assertEqual(pending_states[2]['state'].name, self.state3.name)
# 验证不包含 parameters 字段
self.assertNotIn('parameters', pending_states[0])
# 完成第一个状态
services.advance_to_next_state(self.business_object, self.user)
# 应该只剩下两个待执行节点
pending_states = services.get_all_pending_states(self.business_object)
self.assertEqual(len(pending_states), 2)
self.assertEqual(pending_states[0]['state'].name, self.state2.name)
self.assertEqual(pending_states[1]['state'].name, self.state3.name)
# 完成第二个状态
services.advance_to_next_state(self.business_object, self.user)
# 应该只剩下一个待执行节点
pending_states = services.get_all_pending_states(self.business_object)
self.assertEqual(len(pending_states), 1)
self.assertEqual(pending_states[0]['state'].name, self.state3.name)
# 完成所有状态
services.advance_to_next_state(self.business_object, self.user)
# 应该没有待执行节点
pending_states = services.get_all_pending_states(self.business_object)
self.assertEqual(len(pending_states), 0)
def test_get_state_parameters(self):
"""测试获取节点参数列表(新增功能)"""
# 创建必填和可选参数
required_param = models.StateParameter.objects.create(
key='required_param', value='required_value', is_required=True
)
optional_param1 = models.StateParameter.objects.create(
key='optional_param1', value='optional_value1', is_required=False
)
optional_param2 = models.StateParameter.objects.create(
key='optional_param2', value='optional_value2', is_required=False
)
# 关联参数到状态
self.state1.parameters.add(required_param, optional_param1, optional_param2)
# 获取所有参数
all_params = services.get_state_parameters(self.state1)
self.assertEqual(len(all_params), 3)
# 只获取必填参数
required_params = services.get_state_parameters(self.state1, required_only=True)
self.assertEqual(len(required_params), 1)
self.assertEqual(required_params[0].key, 'required_param')
self.assertTrue(required_params[0].is_required)
# 测试没有参数的状态
no_params = services.get_state_parameters(self.state2)
self.assertEqual(len(no_params), 0)
# 测试只有必填参数的状态
self.state3.parameters.add(required_param)
params = services.get_state_parameters(self.state3, required_only=True)
self.assertEqual(len(params), 1)

View File

@@ -24,9 +24,9 @@ class StepBackTestCase(TestCase):
self.process = models.Process.objects.create(name='测试流程', description='三个状态的测试流程') self.process = models.Process.objects.create(name='测试流程', description='三个状态的测试流程')
# 创建流程节点 # 创建流程节点
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=1) models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=2) models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=3) models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建业务对象 # 创建业务对象
self.business_object = models.BusinessObject.objects.create( self.business_object = models.BusinessObject.objects.create(
@@ -47,13 +47,13 @@ class StepBackTestCase(TestCase):
def test_step_back_from_first_state(self): def test_step_back_from_first_state(self):
"""测试从第一个状态回退到未开始""" """测试从第一个状态回退到未开始"""
# 推进第一个状态 # 推进(完成第一个状态
success, _ = services.advance_to_next_state(self.business_object, self.user) success, _, _ = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success) self.assertTrue(success)
# 验证当前在第一个状态(进行中,准备完成第二个状态) # 验证 current_state 是 state1最后完成的状态)
current_state = services.get_business_object_current_state(self.business_object) current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.id, self.state2.id) self.assertEqual(current_state.name, self.state1.name)
# 回退一步 # 回退一步
success, message = services.step_back_one_state(self.business_object, self.user) success, message = services.step_back_one_state(self.business_object, self.user)
@@ -62,6 +62,8 @@ class StepBackTestCase(TestCase):
# 验证回到未开始状态 # 验证回到未开始状态
self.assertEqual(services.get_overall_status(self.business_object), 'not_started') self.assertEqual(services.get_overall_status(self.business_object), 'not_started')
current_state = services.get_business_object_current_state(self.business_object)
self.assertIsNone(current_state)
# 验证状态1的记录已被撤销 # 验证状态1的记录已被撤销
record = models.StateFlowRecord.objects.filter( record = models.StateFlowRecord.objects.filter(
@@ -73,22 +75,22 @@ class StepBackTestCase(TestCase):
def test_step_back_from_middle_state(self): def test_step_back_from_middle_state(self):
"""测试从中间状态回退""" """测试从中间状态回退"""
# 推进到第二个状态 # 推进两次(完成 state1 和 state2
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
# 验证当前在第二个状态(进行中,准备完成第三个状态) # 验证 current_state 是 state2最后完成的状态)
current_state = services.get_business_object_current_state(self.business_object) current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.id, self.state3.id) self.assertEqual(current_state.name, self.state2.name)
# 回退一步 # 回退一步
success, message = services.step_back_one_state(self.business_object, self.user) success, message = services.step_back_one_state(self.business_object, self.user)
self.assertTrue(success) self.assertTrue(success)
self.assertIn('状态2', message) self.assertIn('状态2', message)
# 验证回到第一个状态(进行中,准备完成第二个状态) # 验证 current_state 变为state1
current_state = services.get_business_object_current_state(self.business_object) current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.id, self.state2.id) self.assertEqual(current_state.name, self.state1.name)
# 验证状态2的记录已被撤销 # 验证状态2的记录已被撤销
record = models.StateFlowRecord.objects.filter( record = models.StateFlowRecord.objects.filter(
@@ -99,23 +101,25 @@ class StepBackTestCase(TestCase):
def test_step_back_from_completed(self): def test_step_back_from_completed(self):
"""测试从已完成状态回退""" """测试从已完成状态回退"""
# 推进到完成 # 推进到完成(完成所有三个状态)
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
# 验证已完成 # 验证已完成current_state 是 state3
self.assertEqual(services.get_overall_status(self.business_object), 'completed') self.assertEqual(services.get_overall_status(self.business_object), 'completed')
current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.name, self.state3.name)
# 回退一步 # 回退一步
success, message = services.step_back_one_state(self.business_object, self.user) success, message = services.step_back_one_state(self.business_object, self.user)
self.assertTrue(success) self.assertTrue(success)
self.assertIn('状态3', message) self.assertIn('状态3', message)
# 验证回到进行中状态(准备完成第三个状态) # 验证回到进行中状态current_state 变为state2
self.assertEqual(services.get_overall_status(self.business_object), 'in_progress') self.assertEqual(services.get_overall_status(self.business_object), 'in_progress')
current_state = services.get_business_object_current_state(self.business_object) current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.id, self.state3.id) self.assertEqual(current_state.name, self.state2.name)
def test_step_back_multiple_times(self): def test_step_back_multiple_times(self):
"""测试多次回退""" """测试多次回退"""
@@ -124,17 +128,17 @@ class StepBackTestCase(TestCase):
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
# 第一次回退 # 第一次回退(撤销 state3
success, _ = services.step_back_one_state(self.business_object, self.user) success, _ = services.step_back_one_state(self.business_object, self.user)
self.assertTrue(success) self.assertTrue(success)
current = services.get_business_object_current_state(self.business_object) current = services.get_business_object_current_state(self.business_object)
self.assertEqual(current.id, self.state3.id) self.assertEqual(current.name, self.state2.name)
# 第二次回退 # 第二次回退(撤销 state2
success, _ = services.step_back_one_state(self.business_object, self.user) success, _ = services.step_back_one_state(self.business_object, self.user)
self.assertTrue(success) self.assertTrue(success)
current = services.get_business_object_current_state(self.business_object) current = services.get_business_object_current_state(self.business_object)
self.assertEqual(current.id, self.state2.id) self.assertEqual(current.name, self.state1.name)
# 第三次回退 # 第三次回退
success, _ = services.step_back_one_state(self.business_object, self.user) success, _ = services.step_back_one_state(self.business_object, self.user)
@@ -148,20 +152,20 @@ class StepBackTestCase(TestCase):
def test_advance_after_step_back(self): def test_advance_after_step_back(self):
"""测试回退后再前进""" """测试回退后再前进"""
# 推进两步 # 推进两步(完成 state1 和 state2
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
services.advance_to_next_state(self.business_object, self.user) services.advance_to_next_state(self.business_object, self.user)
# 回退一步 # 回退一步(撤销 state2
services.step_back_one_state(self.business_object, self.user) services.step_back_one_state(self.business_object, self.user)
# 再前进 # 再前进(重新完成 state2
success, _ = services.advance_to_next_state(self.business_object, self.user) success, _, _ = services.advance_to_next_state(self.business_object, self.user)
self.assertTrue(success) self.assertTrue(success)
# 验证当前在第二个状态 # 验证 current_state 是 state2最后完成的
current_state = services.get_business_object_current_state(self.business_object) current_state = services.get_business_object_current_state(self.business_object)
self.assertEqual(current_state.id, self.state3.id) self.assertEqual(current_state.name, self.state2.name)
def test_step_back_preserves_history(self): def test_step_back_preserves_history(self):
"""测试回退操作保留历史记录""" """测试回退操作保留历史记录"""

View File

@@ -27,9 +27,9 @@ class StepBackAPITestCase(TestCase):
# 创建流程 # 创建流程
self.process = models.Process.objects.create(name='测试流程') self.process = models.Process.objects.create(name='测试流程')
models.ProcessNode.objects.create(process=self.process, state=self.state1, order=1) models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=self.process, state=self.state2, order=2) models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
models.ProcessNode.objects.create(process=self.process, state=self.state3, order=3) models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# 创建业务对象 # 创建业务对象
self.business_object = models.BusinessObject.objects.create( self.business_object = models.BusinessObject.objects.create(