1
0
forked from erp-dev/erp

feat: added process_node parameters view for plate order

This commit is contained in:
2025-12-16 16:15:13 +08:00
parent 0638dd1ad3
commit 639efe8421
7 changed files with 559 additions and 0 deletions

View File

@@ -479,3 +479,195 @@ class BusinessObjectCloneV2APITest(TestCase):
format='json' format='json'
) )
self.assertEqual(resp.status_code, 401) 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)

View File

@@ -5,6 +5,7 @@ from api_v2.views import (
PrintingJobByCustomerView, PrintingJobByCustomerView,
PrintingJobBatchAdvancePreviewView, PrintingJobBatchAdvancePreviewView,
PrintingJobBatchAdvanceSubmitView, PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
BusinessObjectCloneView, BusinessObjectCloneView,
) )
@@ -13,6 +14,7 @@ urlpatterns = [
path('printing-jobs/by-customer/', PrintingJobByCustomerView.as_view(), name='api_v2_printing_job_by_customer'), 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/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('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'), path('stateflow/business-objects/clone/', BusinessObjectCloneView.as_view(), name='api_v2_stateflow_business_object_clone'),
] ]

View File

@@ -8,6 +8,7 @@ from .printing import (
PrintingJobV2Serializer, PrintingJobV2Serializer,
PrintingJobBatchAdvancePreviewView, PrintingJobBatchAdvancePreviewView,
PrintingJobBatchAdvanceSubmitView, PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
) )
from .stateflow import BusinessObjectCloneView from .stateflow import BusinessObjectCloneView
@@ -17,6 +18,7 @@ __all__ = [
'PrintingJobV2Serializer', 'PrintingJobV2Serializer',
'PrintingJobBatchAdvancePreviewView', 'PrintingJobBatchAdvancePreviewView',
'PrintingJobBatchAdvanceSubmitView', 'PrintingJobBatchAdvanceSubmitView',
'PlateOrderByProcessNodeView',
'BusinessObjectCloneView', 'BusinessObjectCloneView',
] ]

View File

@@ -2,13 +2,17 @@ import datetime
from django.utils import timezone from django.utils import timezone
from django.db import transaction 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 import serializers, status, permissions
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from basic_info import models as basic_models from basic_info import models as basic_models
from printing import models as printing_models from printing import models as printing_models
from api_man.serializers import ProductSerializer from api_man.serializers import ProductSerializer
from stateflow import models as stateflow_models
class IsPrintingFactory(permissions.BasePermission): class IsPrintingFactory(permissions.BasePermission):
@@ -360,3 +364,243 @@ class PrintingJobBatchAdvanceSubmitView(APIView):
'printing_job_ids': [j.id for j in data['jobs']], 'printing_job_ids': [j.id for j in data['jobs']],
'jobs': job_srz.data, '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_iddistinct便于计数
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.

View 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 不存在` |