forked from erp-dev/erp
feat: added process_node parameters view for plate order
This commit is contained in:
192
api_v2/tests.py
192
api_v2/tests.py
@@ -479,3 +479,195 @@ class BusinessObjectCloneV2APITest(TestCase):
|
||||
format='json'
|
||||
)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
|
||||
class PlateOrderByProcessNodeV2APITest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# 工厂用户(满足 IsPrintingFactory)
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='开版工厂',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.user = get_user_model().objects.create_user(username='plate_factory_user', password='pass12345')
|
||||
basic_models.Employee.objects.create(
|
||||
merchant=self.merchant,
|
||||
sys_user=self.user,
|
||||
name='开版员工',
|
||||
)
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='客户P',
|
||||
created_by=None,
|
||||
)
|
||||
|
||||
# stateflow:流程 + 3个节点
|
||||
self.state1 = stateflow_models.State.objects.create(name='节点1')
|
||||
self.state2 = stateflow_models.State.objects.create(name='节点2')
|
||||
self.state3 = stateflow_models.State.objects.create(name='节点3')
|
||||
self.param2 = stateflow_models.StateParameter.objects.create(key='temperature', value='25')
|
||||
self.state2.parameters.add(self.param2)
|
||||
|
||||
self.process = stateflow_models.Process.objects.create(name='开版流程')
|
||||
self.node1 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
|
||||
self.node2 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
|
||||
self.node3 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
|
||||
|
||||
# PlateOrder:构造不同“下一待执行节点”
|
||||
self.po_state1 = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
process=self.process.id,
|
||||
design_code='PO-A',
|
||||
style_name='款式A',
|
||||
)
|
||||
|
||||
self.po_state2_old = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
process=self.process.id,
|
||||
design_code='CODE-X', # 不包含数字,便于测试 search=id
|
||||
style_name='款式B',
|
||||
)
|
||||
stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=self.po_state2_old.business_object,
|
||||
state=self.state1,
|
||||
completed_by=self.user,
|
||||
is_cancelled=False,
|
||||
)
|
||||
# 订单维度参数:模拟该订单曾在 state2 提交过参数,但该状态记录已撤销(因此当前仍处于 state2 待执行)
|
||||
cancelled_log = stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=self.po_state2_old.business_object,
|
||||
state=self.state2,
|
||||
completed_by=self.user,
|
||||
is_cancelled=True,
|
||||
)
|
||||
stateflow_models.StateLogParameterRecord.objects.create(
|
||||
state_log=cancelled_log,
|
||||
parameters={'temperature': '30'},
|
||||
)
|
||||
|
||||
self.po_state2_new = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
process=self.process.id,
|
||||
design_code='ABC-999',
|
||||
style_name='款式C',
|
||||
)
|
||||
stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=self.po_state2_new.business_object,
|
||||
state=self.state1,
|
||||
completed_by=self.user,
|
||||
is_cancelled=False,
|
||||
)
|
||||
|
||||
self.po_state3 = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
process=self.process.id,
|
||||
design_code='PO-D',
|
||||
style_name='款式D',
|
||||
)
|
||||
stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=self.po_state3.business_object,
|
||||
state=self.state1,
|
||||
completed_by=self.user,
|
||||
is_cancelled=False,
|
||||
)
|
||||
stateflow_models.StateFlowRecord.objects.create(
|
||||
business_object=self.po_state3.business_object,
|
||||
state=self.state2,
|
||||
completed_by=self.user,
|
||||
is_cancelled=False,
|
||||
)
|
||||
|
||||
# 固定 created_at 用于排序断言
|
||||
tz = timezone.get_default_timezone()
|
||||
t_old = timezone.make_aware(datetime.datetime(2025, 12, 1, 10, 0, 0), tz)
|
||||
t_new = timezone.make_aware(datetime.datetime(2025, 12, 2, 10, 0, 0), tz)
|
||||
printing_models.PlateOrder.objects.filter(id=self.po_state2_old.id).update(created_at=t_old)
|
||||
printing_models.PlateOrder.objects.filter(id=self.po_state2_new.id).update(created_at=t_new)
|
||||
|
||||
self.url = '/api/v2/plate-orders/by-process-node/'
|
||||
|
||||
def test_filter_by_process_node_returns_only_matching_next_pending(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data['process_node']['id'], self.node2.id)
|
||||
self.assertEqual(resp.data['process_node']['state_id'], self.state2.id)
|
||||
self.assertTrue(any(p['key'] == 'temperature' and p['value'] == '25' for p in resp.data['parameters']))
|
||||
|
||||
ids = [item['id'] for item in resp.data['results']]
|
||||
self.assertEqual(set(ids), {self.po_state2_old.id, self.po_state2_new.id})
|
||||
|
||||
# results 必须包含“订单维度”的参数值(同一节点下不同订单可不同)
|
||||
by_id = {item['id']: item for item in resp.data['results']}
|
||||
self.assertIn('process_parameters', by_id[self.po_state2_old.id])
|
||||
self.assertIn('process_parameters', by_id[self.po_state2_new.id])
|
||||
|
||||
# po_state2_old:来自已撤销的 state2 提交记录
|
||||
self.assertTrue(
|
||||
any(p['key'] == 'temperature' and p['value'] == '30' for p in by_id[self.po_state2_old.id]['process_parameters'])
|
||||
)
|
||||
# po_state2_new:从未提交过 state2 参数,value 应为 null
|
||||
self.assertTrue(
|
||||
any(p['key'] == 'temperature' and p['value'] is None for p in by_id[self.po_state2_new.id]['process_parameters'])
|
||||
)
|
||||
|
||||
def test_process_parameters_schema_and_keys_align_with_template(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
template_keys = [p['key'] for p in resp.data['parameters']]
|
||||
self.assertGreater(len(template_keys), 0)
|
||||
|
||||
for item in resp.data['results']:
|
||||
self.assertIn('process_parameters', item)
|
||||
self.assertIsInstance(item['process_parameters'], list)
|
||||
|
||||
keys = [p.get('key') for p in item['process_parameters']]
|
||||
self.assertEqual(keys, template_keys)
|
||||
|
||||
# 严格 schema:每项只能有 key/value 两个字段(便于前端做稳定类型定义)
|
||||
for p in item['process_parameters']:
|
||||
self.assertEqual(set(p.keys()), {'key', 'value'})
|
||||
|
||||
def test_when_state_has_no_parameters_returns_empty_process_parameters(self):
|
||||
# node1 对应 state1:该测试 setUp 中未配置任何参数
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node1.id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data['parameters'], [])
|
||||
self.assertEqual(len(resp.data['results']), 1)
|
||||
self.assertEqual(resp.data['results'][0]['id'], self.po_state1.id)
|
||||
self.assertEqual(resp.data['results'][0]['process_parameters'], [])
|
||||
|
||||
def test_default_ordering_is_minus_created_at(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ids = [item['id'] for item in resp.data['results']]
|
||||
self.assertEqual(ids[0], self.po_state2_new.id)
|
||||
self.assertEqual(ids[1], self.po_state2_old.id)
|
||||
|
||||
def test_ordering_param_created_at_asc(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'ordering': 'created_at'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ids = [item['id'] for item in resp.data['results']]
|
||||
self.assertEqual(ids[0], self.po_state2_old.id)
|
||||
self.assertEqual(ids[1], self.po_state2_new.id)
|
||||
|
||||
def test_search_supports_design_code_icontains(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': 'abc'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ids = [item['id'] for item in resp.data['results']]
|
||||
self.assertEqual(ids, [self.po_state2_new.id])
|
||||
|
||||
def test_search_supports_pk_without_extra_param(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': str(self.po_state2_old.id)})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
ids = [item['id'] for item in resp.data['results']]
|
||||
self.assertEqual(ids, [self.po_state2_old.id])
|
||||
|
||||
def test_pagination_limit_offset(self):
|
||||
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'limit': 1, 'offset': 0})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data['count'], 2)
|
||||
self.assertEqual(len(resp.data['results']), 1)
|
||||
|
||||
@@ -5,6 +5,7 @@ from api_v2.views import (
|
||||
PrintingJobByCustomerView,
|
||||
PrintingJobBatchAdvancePreviewView,
|
||||
PrintingJobBatchAdvanceSubmitView,
|
||||
PlateOrderByProcessNodeView,
|
||||
BusinessObjectCloneView,
|
||||
)
|
||||
|
||||
@@ -13,6 +14,7 @@ urlpatterns = [
|
||||
path('printing-jobs/by-customer/', PrintingJobByCustomerView.as_view(), name='api_v2_printing_job_by_customer'),
|
||||
path('printing-jobs/batch-advance/preview/', PrintingJobBatchAdvancePreviewView.as_view(), name='api_v2_printing_job_batch_advance_preview'),
|
||||
path('printing-jobs/batch-advance/', PrintingJobBatchAdvanceSubmitView.as_view(), name='api_v2_printing_job_batch_advance_submit'),
|
||||
path('plate-orders/by-process-node/', PlateOrderByProcessNodeView.as_view(), name='api_v2_plate_order_by_process_node'),
|
||||
path('stateflow/business-objects/clone/', BusinessObjectCloneView.as_view(), name='api_v2_stateflow_business_object_clone'),
|
||||
]
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from .printing import (
|
||||
PrintingJobV2Serializer,
|
||||
PrintingJobBatchAdvancePreviewView,
|
||||
PrintingJobBatchAdvanceSubmitView,
|
||||
PlateOrderByProcessNodeView,
|
||||
)
|
||||
from .stateflow import BusinessObjectCloneView
|
||||
|
||||
@@ -17,6 +18,7 @@ __all__ = [
|
||||
'PrintingJobV2Serializer',
|
||||
'PrintingJobBatchAdvancePreviewView',
|
||||
'PrintingJobBatchAdvanceSubmitView',
|
||||
'PlateOrderByProcessNodeView',
|
||||
'BusinessObjectCloneView',
|
||||
]
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@ import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, Count, CharField, Prefetch
|
||||
from django.db.models.functions import Cast, Coalesce
|
||||
from rest_framework import serializers, status, permissions
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from api_man.serializers import ProductSerializer
|
||||
from stateflow import models as stateflow_models
|
||||
|
||||
|
||||
class IsPrintingFactory(permissions.BasePermission):
|
||||
@@ -360,3 +364,243 @@ class PrintingJobBatchAdvanceSubmitView(APIView):
|
||||
'printing_job_ids': [j.id for j in data['jobs']],
|
||||
'jobs': job_srz.data,
|
||||
})
|
||||
|
||||
|
||||
class PlateOrderByProcessNodeSerializer(serializers.ModelSerializer):
|
||||
"""按流程节点筛选 PlateOrder 的列表序列化(尽量保持轻量)"""
|
||||
|
||||
design_code = serializers.SerializerMethodField()
|
||||
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||
business_object_id = serializers.SerializerMethodField()
|
||||
process_parameters = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = printing_models.PlateOrder
|
||||
fields = [
|
||||
'id',
|
||||
'design_code',
|
||||
'customer',
|
||||
'customer_name',
|
||||
'style_name',
|
||||
'urgency_level',
|
||||
'is_invalid',
|
||||
'business_object_id',
|
||||
'process_parameters',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def get_design_code(self, obj: printing_models.PlateOrder) -> str | None:
|
||||
return obj.design_code or (str(obj.id) if obj.id else None)
|
||||
|
||||
def get_business_object_id(self, obj: printing_models.PlateOrder) -> int | None:
|
||||
return obj.business_object_id
|
||||
|
||||
def get_process_parameters(self, obj: printing_models.PlateOrder) -> list[dict]:
|
||||
"""
|
||||
返回“订单维度”的工艺参数 key/value(当前 process_node 对应 state 的参数)。
|
||||
|
||||
取值来源:
|
||||
- 优先取该订单 business_object 在目标 state 的**最新一次 StateFlowRecord**(可能是已撤销记录)
|
||||
对应的 StateLogParameterRecord 汇总(后提交覆盖先提交)。
|
||||
- 若从未提交过该 state 的参数:value 为 null(由前端自行用顶层 parameters 的默认值做兜底/占位)
|
||||
"""
|
||||
keys: list[str] = self.context.get('target_parameter_keys') or []
|
||||
target_state_id: int | None = self.context.get('target_state_id')
|
||||
if not keys or not target_state_id:
|
||||
return [{'key': k, 'value': None} for k in keys]
|
||||
|
||||
bo = getattr(obj, 'business_object', None)
|
||||
if not bo:
|
||||
return [{'key': k, 'value': None} for k in keys]
|
||||
|
||||
logs = getattr(bo, '_prefetched_target_state_logs', None)
|
||||
if logs is None:
|
||||
# fallback:极少数情况下未预取
|
||||
logs = list(
|
||||
bo.state_logs.filter(state_id=target_state_id).order_by('-completed_at', '-id')[:1]
|
||||
)
|
||||
|
||||
latest_log = logs[0] if logs else None
|
||||
summary: dict = {}
|
||||
if latest_log:
|
||||
param_records = getattr(latest_log, '_prefetched_parameter_records', None)
|
||||
if param_records is None:
|
||||
param_records = list(latest_log.parameter_records.all().order_by('created_at', 'id'))
|
||||
for rec in param_records:
|
||||
summary.update(rec.parameters or {})
|
||||
|
||||
return [{'key': k, 'value': summary.get(k)} for k in keys]
|
||||
|
||||
|
||||
class PlateOrderByProcessNodeView(APIView):
|
||||
"""
|
||||
按 process_node_id 查询“当前处于该节点(NEXT 模式:下一个待执行节点)”的 PlateOrder 列表。
|
||||
|
||||
GET /api/v2/plate-orders/by-process-node/
|
||||
|
||||
Query 参数:
|
||||
- process_node_id: 必填,ProcessNode.id
|
||||
- search: 可选。支持:
|
||||
- 纯数字:同时匹配 id 精确 + design_code icontains
|
||||
- 非纯数字:design_code icontains
|
||||
- ordering: 可选,默认 -created_at,支持: id / created_at / updated_at / design_code
|
||||
- limit/offset: 分页(limit 默认 20)
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
|
||||
|
||||
_ORDERING_FIELDS = {'id', 'created_at', 'updated_at', 'design_code'}
|
||||
|
||||
def get(self, request):
|
||||
qp = request.query_params
|
||||
process_node_id = qp.get('process_node_id')
|
||||
if not process_node_id:
|
||||
return Response({'detail': 'process_node_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
process_node_id_int = int(process_node_id)
|
||||
except (TypeError, ValueError):
|
||||
return Response({'detail': 'process_node_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
process_node = (
|
||||
stateflow_models.ProcessNode.objects
|
||||
.select_related('process', 'state')
|
||||
.get(id=process_node_id_int)
|
||||
)
|
||||
except stateflow_models.ProcessNode.DoesNotExist:
|
||||
return Response({'detail': 'process_node 不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# 目标节点信息
|
||||
target_state_id = process_node.state_id
|
||||
target_order = process_node.order
|
||||
process_id = process_node.process_id
|
||||
|
||||
# 目标节点之前的所有 state_id(distinct,便于计数)
|
||||
prev_state_ids = list(
|
||||
stateflow_models.ProcessNode.objects
|
||||
.filter(process_id=process_id, order__lt=target_order)
|
||||
.order_by('order', 'id')
|
||||
.values_list('state_id', flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
queryset = (
|
||||
printing_models.PlateOrder.objects
|
||||
.select_related('customer', 'business_object')
|
||||
.filter(business_object__isnull=False, business_object__process_id=process_id)
|
||||
)
|
||||
|
||||
# NEXT 模式判定:前置节点都已完成(未撤销) + 目标节点尚未完成(未撤销)
|
||||
if prev_state_ids:
|
||||
queryset = queryset.annotate(
|
||||
completed_prev_states=Count(
|
||||
'business_object__state_logs__state_id',
|
||||
filter=Q(
|
||||
business_object__state_logs__is_cancelled=False,
|
||||
business_object__state_logs__state_id__in=prev_state_ids,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
).filter(completed_prev_states=len(prev_state_ids))
|
||||
|
||||
queryset = queryset.annotate(
|
||||
completed_target=Count(
|
||||
'business_object__state_logs__id',
|
||||
filter=Q(
|
||||
business_object__state_logs__is_cancelled=False,
|
||||
business_object__state_logs__state_id=target_state_id,
|
||||
),
|
||||
distinct=True,
|
||||
)
|
||||
).filter(completed_target=0)
|
||||
|
||||
# search:同时支持主键与 design_code icontains(不新增额外参数)
|
||||
search = (qp.get('search') or '').strip()
|
||||
if search:
|
||||
if search.isdigit():
|
||||
try:
|
||||
search_id = int(search)
|
||||
except (TypeError, ValueError):
|
||||
search_id = None
|
||||
cond = Q(design_code__icontains=search)
|
||||
if search_id is not None:
|
||||
cond = cond | Q(id=search_id)
|
||||
queryset = queryset.filter(cond)
|
||||
else:
|
||||
queryset = queryset.filter(design_code__icontains=search)
|
||||
|
||||
# ordering:默认 -created_at
|
||||
ordering = (qp.get('ordering') or '-created_at').strip() or '-created_at'
|
||||
direction = '-' if ordering.startswith('-') else ''
|
||||
field = ordering[1:] if ordering.startswith('-') else ordering
|
||||
if field not in self._ORDERING_FIELDS:
|
||||
return Response(
|
||||
{'detail': f'ordering 不支持: {ordering}(可选: {", ".join(sorted(self._ORDERING_FIELDS))})'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# 为 design_code 排序提供兜底:为空时按主键字符串
|
||||
if field == 'design_code':
|
||||
queryset = queryset.annotate(
|
||||
design_code_normalized=Coalesce('design_code', Cast('id', output_field=CharField()))
|
||||
).order_by(f'{direction}design_code_normalized', 'id')
|
||||
else:
|
||||
queryset = queryset.order_by(f'{direction}{field}', 'id')
|
||||
|
||||
# 该节点参数模板(仅 key/value),同时用于 results[*].process_parameters 的 key 集合
|
||||
params = list(process_node.state.parameters.order_by('id').values('key', 'value'))
|
||||
target_keys = [p['key'] for p in params]
|
||||
|
||||
# 预取:目标 state 的最新日志及其参数记录(用于拼装“订单维度”的参数值,避免 N+1)
|
||||
param_records_prefetch = Prefetch(
|
||||
'parameter_records',
|
||||
queryset=stateflow_models.StateLogParameterRecord.objects.order_by('created_at', 'id'),
|
||||
to_attr='_prefetched_parameter_records',
|
||||
)
|
||||
target_state_logs_qs = (
|
||||
stateflow_models.StateFlowRecord.objects
|
||||
.filter(state_id=target_state_id)
|
||||
.order_by('-completed_at', '-id')
|
||||
.prefetch_related(param_records_prefetch)
|
||||
)
|
||||
queryset = queryset.prefetch_related(
|
||||
Prefetch(
|
||||
'business_object__state_logs',
|
||||
queryset=target_state_logs_qs,
|
||||
to_attr='_prefetched_target_state_logs',
|
||||
)
|
||||
)
|
||||
|
||||
# 分页(limit 默认 20)
|
||||
paginator = LimitOffsetPagination()
|
||||
paginator.default_limit = 20
|
||||
page = paginator.paginate_queryset(queryset, request, view=self)
|
||||
results = page if page is not None else list(queryset)
|
||||
|
||||
srz = PlateOrderByProcessNodeSerializer(
|
||||
results,
|
||||
many=True,
|
||||
context={
|
||||
'request': request,
|
||||
'target_state_id': target_state_id,
|
||||
'target_parameter_keys': target_keys,
|
||||
},
|
||||
)
|
||||
|
||||
return Response({
|
||||
'process_node': {
|
||||
'id': process_node.id,
|
||||
'process_id': process_id,
|
||||
'state_id': target_state_id,
|
||||
'state_name': process_node.state.name,
|
||||
'order': target_order,
|
||||
},
|
||||
'parameters': params,
|
||||
'count': getattr(paginator, 'count', len(results)),
|
||||
'next': paginator.get_next_link() if page is not None else None,
|
||||
'previous': paginator.get_previous_link() if page is not None else None,
|
||||
'results': srz.data,
|
||||
})
|
||||
|
||||
Binary file not shown.
Binary file not shown.
119
docs/api_v2_plate_orders_by_process_node.md
Normal file
119
docs/api_v2_plate_orders_by_process_node.md
Normal file
@@ -0,0 +1,119 @@
|
||||
## api_v2:按流程节点查询 PlateOrder 列表(含该节点工艺参数 key/value)
|
||||
|
||||
### 目标
|
||||
- **给前端提供**:按指定 `process_node_id` 拉取“当前处于该流程节点”的 `PlateOrder` 列表。
|
||||
- **同时返回**:
|
||||
- 该节点(`State`)关联的工艺参数**模板默认值**(key/value)
|
||||
- **每个订单在该节点上的参数值**(key/value,订单维度;若从未提交则为 null)
|
||||
|
||||
### “处于该节点”的判定语义(与 stateflow 现有实现保持一致)
|
||||
- 本接口采用 **NEXT 语义**:`process_node` 对应的 `state` 是该 `PlateOrder.business_object` 的 **下一个待执行节点**。
|
||||
- 等价规则(忽略已撤销记录 `is_cancelled=True`):
|
||||
- 该节点之前(同一 `process`、`order` 更小)的所有节点均已完成(存在未撤销的 `StateFlowRecord`)
|
||||
- 该节点本身尚未完成(不存在未撤销的 `StateFlowRecord`)
|
||||
- 仅查询满足以下条件的订单:
|
||||
- `PlateOrder.business_object` 存在
|
||||
- `PlateOrder.business_object.process_id == process_node.process_id`
|
||||
|
||||
### 接口信息
|
||||
- **Method**:GET
|
||||
- **Path**:`/api/v2/plate-orders/by-process-node/`
|
||||
- **认证**:JWT(`IsAuthenticated`)
|
||||
- **权限**:仅允许印染/工厂侧用户(与 v2 printing 其它接口一致,`IsPrintingFactory`)
|
||||
|
||||
### Query 参数
|
||||
| 参数 | 必填 | 类型 | 默认值 | 说明 |
|
||||
|---|---:|---|---|---|
|
||||
| `process_node_id` | 是 | int | - | `stateflow.ProcessNode.id` |
|
||||
| `search` | 否 | string | - | 单一搜索参数:同时支持主键与设计编号(见“查询规则”) |
|
||||
| `ordering` | 否 | string | `-created_at` | 排序字段(见“排序规则”) |
|
||||
| `limit` | 否 | int | `20` | 分页大小(LimitOffsetPagination) |
|
||||
| `offset` | 否 | int | `0` | 分页偏移(LimitOffsetPagination) |
|
||||
|
||||
### 查询规则(search)
|
||||
- 当 `search` 为**纯数字**:
|
||||
- 匹配 `PlateOrder.id == int(search)` **或**
|
||||
- 匹配 `PlateOrder.design_code icontains search`
|
||||
- 当 `search` 为**非纯数字**:
|
||||
- 仅匹配 `PlateOrder.design_code icontains search`
|
||||
|
||||
### 排序规则(ordering)
|
||||
- 默认:`-created_at`
|
||||
- 支持字段:`id` / `created_at` / `updated_at` / `design_code`
|
||||
- `design_code` 排序规则:
|
||||
- 当 `design_code` 为空时,使用 `id` 的字符串作为兜底值参与排序(保证排序稳定、可预测)
|
||||
- 传入不支持的 `ordering`:返回 **400**
|
||||
|
||||
### 返回值(200)
|
||||
响应为分页结构,并额外附带节点信息与该节点工艺参数 key/value。
|
||||
|
||||
#### 顶层字段
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `process_node` | object | 目标流程节点信息(见下) |
|
||||
| `parameters` | array[object] | 该节点 `State.parameters` 的 key/value(**模板默认值**;不是订单提交值) |
|
||||
| `count` | int | 满足条件的总数 |
|
||||
| `next` | string\|null | 下一页链接 |
|
||||
| `previous` | string\|null | 上一页链接 |
|
||||
| `results` | array[object] | 当前页的 `PlateOrder` 列表(见下) |
|
||||
|
||||
#### `process_node` 字段
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | int | `ProcessNode.id` |
|
||||
| `process_id` | int | `Process.id` |
|
||||
| `state_id` | int | `State.id` |
|
||||
| `state_name` | string | `State.name` |
|
||||
| `order` | int | 节点顺序号 |
|
||||
|
||||
#### `parameters` 元素字段(仅 key/value)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `key` | string | 参数键 |
|
||||
| `value` | string\|null | 参数默认值(如有) |
|
||||
|
||||
#### `process_parameters` 元素字段(订单维度:严格 schema)
|
||||
> 该字段存在于 `results[*].process_parameters`,用于“每个订单在当前节点上的参数值”展示/回显。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `key` | string | 参数键;**与顶层 `parameters[*].key` 一致** |
|
||||
| `value` | JSONValue\|null | 该订单在此节点的最新已提交参数值;若从未提交过该节点参数,则为 null |
|
||||
|
||||
**JSONValue 定义**(用于前端类型定义):
|
||||
- `string` \| `number` \| `boolean` \| `object` \| `array` \| `null`
|
||||
|
||||
**不变性约束(前端可依赖)**:
|
||||
- 对任意返回的 `PlateOrder`:
|
||||
- `process_parameters` **一定存在**,类型恒为 `array`
|
||||
- `process_parameters` 的 **key 集合与顺序**与顶层 `parameters` **完全一致**
|
||||
- 若该节点无任何参数:`parameters=[]` 且 `process_parameters=[]`
|
||||
|
||||
**取值规则(value 来源)**:
|
||||
- 取该订单 `business_object` 在目标 `state` 的**最新一条** `StateFlowRecord`(按 `completed_at` 倒序,`id` 倒序)关联的参数提交汇总;同一条状态日志下多次补充参数时,后提交覆盖先提交。
|
||||
- 若该节点当前为“待执行”(NEXT),通常不会有未撤销的完成日志;当订单曾完成过该节点但后续回退时,会存在已撤销的日志,此时仍可能带有历史参数值用于回显。
|
||||
|
||||
#### `results` 元素字段(PlateOrder 列表项)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | int | PlateOrder 主键 |
|
||||
| `design_code` | string\|null | 设计编号;若为空则返回 `id` 的字符串兜底值 |
|
||||
| `customer` | int | 客户 ID |
|
||||
| `customer_name` | string | 客户名称 |
|
||||
| `style_name` | string\|null | 款号名称 |
|
||||
| `urgency_level` | string | 紧急程度 |
|
||||
| `is_invalid` | bool | 是否作废 |
|
||||
| `business_object_id` | int\|null | 关联流程实例 ID |
|
||||
| `process_parameters` | array[object] | **订单维度**的该节点参数 key/value(与顶层 `parameters` 的 key 集合一致;若该订单从未提交过该节点参数,则对应 value 为 null) |
|
||||
| `created_at` | string | 创建时间(ISO 8601) |
|
||||
| `updated_at` | string | 更新时间(ISO 8601) |
|
||||
|
||||
### 常见错误码
|
||||
| HTTP 状态码 | 场景 | 返回 `detail` |
|
||||
|---:|---|---|
|
||||
| 400 | 缺少/非法参数(如 `process_node_id` 非数字、`ordering` 不支持) | 错误原因文本 |
|
||||
| 401 | 未认证(缺少/无效 JWT) | DRF 默认 |
|
||||
| 403 | 已认证但无权限(非工厂用户) | `您没有访问印染订单的权限` |
|
||||
| 404 | `process_node_id` 不存在 | `process_node 不存在` |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user