From dff91516518d6d95803e3ab3e1b83dc3c7a0527b Mon Sep 17 00:00:00 2001 From: colaftc Date: Mon, 17 Nov 2025 12:11:04 +0800 Subject: [PATCH] fix: admin of printing module --- .idea/.gitignore | 8 + .idea/copilot.data.migration.agent.xml | 6 + .idea/copilot.data.migration.ask.xml | 6 + .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/copilot.data.migration.edit.xml | 6 + .idea/flower.iml | 29 ++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + api_v1/views/printing/serializers.py | 7 - printing/admin.py | 4 +- stateflow/API.md | 275 ++++++++++++++++++ stateflow/admin.py | 103 ++++++- stateflow/tests/test_services.py | 22 +- 15 files changed, 473 insertions(+), 23 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/copilot.data.migration.agent.xml create mode 100644 .idea/copilot.data.migration.ask.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/copilot.data.migration.edit.xml create mode 100644 .idea/flower.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/flower.iml b/.idea/flower.iml new file mode 100644 index 0000000..1fd8baa --- /dev/null +++ b/.idea/flower.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c2a212e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..cf45279 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index 1569072..8bc4730 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -175,12 +175,6 @@ class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer): raise serializers.ValidationError("数量必须大于0") return value - def validate_pieces(self, value): - """验证件数(仅在提供值时验证)""" - if value is not None and value <= 0: - raise serializers.ValidationError("件数必须大于0") - return value - def create(self, validated_data): """创建任务,使用 service 层""" user = self.context['request'].user @@ -197,7 +191,6 @@ class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer): return updated_instance - class PlateOrderListSerializer(serializers.ModelSerializer): """开版订单列表序列化器""" customer_name = serializers.CharField(source="customer.name", read_only=True) diff --git a/printing/admin.py b/printing/admin.py index 96bbab5..14e62cf 100644 --- a/printing/admin.py +++ b/printing/admin.py @@ -132,7 +132,7 @@ class PlateOrderAdmin(admin.ModelAdmin): messages.append(f"{obj.id}: 没有关联的流程实例,无法推进") continue - success, message, _ = services.advance_to_next_state(obj.business_object, request.user) + success, message, state_log = services.advance_to_next_state(obj.business_object, request.user) if success: success_count += 1 messages.append(f"{obj.id}: {message}") @@ -383,7 +383,7 @@ class PrintingJobAdmin(admin.ModelAdmin): messages.append(f"PrintingJob #{obj.id}: 没有关联的业务对象,无法推进") continue - success, message = services.advance_to_next_state(obj.business_object, request.user) + success, message, state_log = services.advance_to_next_state(obj.business_object, request.user) if success: success_count += 1 messages.append(f"PrintingJob #{obj.id}: {message}") diff --git a/stateflow/API.md b/stateflow/API.md index c46e76d..2cfd9b9 100644 --- a/stateflow/API.md +++ b/stateflow/API.md @@ -14,10 +14,285 @@ Stateflow API 提供了状态节点和流程的 CRUD 操作接口。 所有 API 都需要 JWT 认证。在请求头中添加: +# Stateflow API 文档 (v2) + +## 概述 + +Stateflow API 提供了状态节点、流程模板以及流程实例(业务对象)的完整生命周期管理,包括创建、查询、更新、删除以及核心的状态流转操作。 + +## 基础 URL + +所有 Stateflow 相关 API 都在以下基础路径下: + +``` +/api/v1/stateflow/ +``` + +## 认证 + +所有 API 都需要 JWT 认证。在请求头中添加: + ``` Authorization: Bearer ``` +## 分页 + +列表接口默认使用 `LimitOffset` 分页: + +- `limit`: 返回结果数量。 +- `offset`: 偏移量。 + +--- + +## 1. State API (状态节点) + +**基础路径**: `/api/v1/stateflow/states/` + +管理流程中的基础单元:状态。 + +### 1.1. 列表查询 + +- **GET** `/api/v1/stateflow/states/` +- **描述**: 获取状态节点列表。 +- **查询参数**: + - `name`: 按名称精确查询。 + - `search`: 按名称和描述进行模糊搜索。 + - `ordering`: 排序字段 (`id`, `name`, `created_at`, `updated_at`)。 + +### 1.2. 详情查询 + +- **GET** `/api/v1/stateflow/states/{id}/` +- **描述**: 获取单个状态节点的详细信息,包含其关联的参数模板。 + +### 1.3. 创建状态 + +- **POST** `/api/v1/stateflow/states/` +- **描述**: 创建一个新的状态节点。 +- **请求体**: + ```json + { + "name": "待质检", + "description": "等待品质检验", + "parameters": [ + { + "key": "inspector", + "value": "", + "is_required": true, + "description": "质检员" + } + ] + } + ``` + +### 1.4. 更新状态 + +- **PUT/PATCH** `/api/v1/stateflow/states/{id}/` +- **描述**: 完全或部分更新一个状态节点。`PUT` 请求会替换所有字段,`PATCH` 只更新提供的字段。`parameters` 数组在 `PUT` 时会完全替换。 + +### 1.5. 删除状态 + +- **DELETE** `/api/v1/stateflow/states/{id}/` +- **描述**: 删除一个状态节点。 + +### 1.6. 获取状态参数 + +- **GET** `/api/v1/stateflow/states/{id}/parameters/` +- **描述**: 获取指定状态的参数列表。 +- **查询参数**: + - `required_only`: `true` 或 `false`,是否只返回必填参数。 + +--- + +## 2. Process API (流程模板) + +**基础路径**: `/api/v1/stateflow/processes/` + +管理可复用的流程模板。 + +### 2.1. 列表查询 + +- **GET** `/api/v1/stateflow/processes/` +- **描述**: 获取流程模板列表。 +- **查询参数**: + - `name`: 按名称精确查询。 + - `search`: 按名称和描述进行模糊搜索。 + - `ordering`: 排序字段 (`id`, `name`, `node_count`, `created_at`, `updated_at`)。 + +### 2.2. 详情查询 + +- **GET** `/api/v1/stateflow/processes/{id}/` +- **描述**: 获取单个流程模板的详细信息,包含其所有节点(`nodes`)。 + +### 2.3. 创建流程 + +- **POST** `/api/v1/stateflow/processes/` +- **描述**: 创建一个新的流程模板,并定义其节点顺序。 +- **请求体**: + ```json + { + "name": "生产流程", + "description": "从下单到发货的标准流程", + "nodes": [ + { "state_id": 1, "order": 0 }, + { "state_id": 2, "order": 1 }, + { "state_id": 5, "order": 2 } + ] + } + ``` + +### 2.4. 更新流程 + +- **PUT/PATCH** `/api/v1/stateflow/processes/{id}/` +- **描述**: 更新流程模板。`nodes` 数组在 `PUT` 时会完全替换。 + +### 2.5. 删除流程 + +- **DELETE** `/api/v1/stateflow/processes/{id}/` +- **描述**: 删除一个流程模板。 + +--- + +## 3. Business Object API (流程实例) + +**基础路径**: `/api/v1/stateflow/business-objects/` + +管理流程的具体实例,这是状态流转的核心。 + +### 3.1. 列表查询 + +- **GET** `/api/v1/stateflow/business-objects/` +- **描述**: 获取流程实例列表。 +- **查询参数**: + - `name`: 按名称模糊查询。 + - `process`: 按流程模板 ID 过滤。 + - `process_name`: 按流程模板名称模糊查询。 + - `overall_status`: 按整体状态过滤 (`not_started`, `in_progress`, `completed`)。 + - `content_type_str`: 按关联的业务模型过滤,格式: `app_label.model` (例如 `printing.plateorder`)。 + - `has_content_object`: `true` 或 `false`,过滤是否有关联的业务模型。 + - `search`: 按名称和描述进行模糊搜索。 + - `ordering`: 排序字段 (`id`, `name`, `created_at`, `updated_at`)。 + +### 3.2. 详情查询 + +- **GET** `/api/v1/stateflow/business-objects/{id}/` +- **描述**: 获取单个流程实例的详细信息,包含当前状态、进度、时间线等。 + +### 3.3. 创建流程实例 + +- **POST** `/api/v1/stateflow/business-objects/` +- **描述**: 创建一个新的流程实例。 +- **请求体**: + ```json + { + "name": "订单 #123 的生产流程", + "process": 1, + "description": "客户A的加急订单" + } + ``` + +### 3.4. 更新流程实例 + +- **PUT/PATCH** `/api/v1/stateflow/business-objects/{id}/` +- **描述**: 更新流程实例的基本信息(如名称、描述)。 + +### 3.5. 删除流程实例 + +- **DELETE** `/api/v1/stateflow/business-objects/{id}/` +- **描述**: 删除一个流程实例及其所有相关日志。 + +### 3.6. 核心流转操作 (Custom Actions) + +#### 3.6.1. 推进到下一状态 + +- **POST** `/api/v1/stateflow/business-objects/{id}/advance/` +- **描述**: 将流程实例从当前状态推进到下一个状态。 +- **请求体** (可选): + ```json + { + "parameters": { + "temperature": "25.5", + "humidity": "60%" + } + } + ``` +- **成功响应**: `200 OK`,包含成功信息和更新后的业务对象。 +- **失败响应**: `400 Bad Request`,如果缺少必填参数或流程已完成。 + ```json + { + "success": false, + "message": "流程已完成,无法继续推进" + } + ``` + +#### 3.6.2. 回退一步 + +- **POST** `/api/v1/stateflow/business-objects/{id}/step_back/` +- **描述**: 撤销最后一次完成的状态,使流程回退一步。 +- **成功响应**: `200 OK`。 +- **失败响应**: `400 Bad Request`,如果从未开始过。 + ```json + { + "success": false, + "message": "没有任何状态流转记录,无法回退" + } + ``` + +#### 3.6.3. 重置进度 + +- **POST** `/api/v1/stateflow/business-objects/{id}/reset/` +- **描述**: 撤销所有已完成的状态,将流程实例重置到未开始状态。 + +### 3.7. 状态查询接口 (Custom Actions) + +#### 3.7.1. 获取状态时间线 + +- **GET** `/api/v1/stateflow/business-objects/{id}/timeline/` +- **描述**: 获取一个包含所有节点及其当前状态(`not_started`, `in_progress`, `completed`, `cancelled`)的时间线。 + +#### 3.7.2. 获取下一个待执行节点 + +- **GET** `/api/v1/stateflow/business-objects/{id}/next_pending_state/` +- **描述**: 获取下一个待执行的节点信息。 +- **查询参数**: + - `include_parameters`: `true` 或 `false`,是否包含节点的参数模板。 + +#### 3.7.3. 获取所有待执行节点 + +- **GET** `/api/v1/stateflow/business-objects/{id}/pending_states/` +- **描述**: 获取所有未完成的节点列表。 + +#### 3.7.4. 获取当前状态的参数 + +- **GET** `/api/v1/stateflow/business-objects/{id}/current_state_parameters/` +- **描述**: 获取当前待执行节点(`current_state`)的参数模板。 +- **注意**: `current_state` 指的是下一个待执行的节点。如果流程已完成,则返回空。 + +### 3.8. 参数与日志接口 (Custom Actions) + +#### 3.8.1. 为日志补充参数 + +- **POST** `/api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/add-parameters/` +- **描述**: 为某一次具体的状态流转记录(`StateFlowRecord`)补充额外的参数。 +- **请求体**: + ```json + { + "parameters": { + "inspector_comment": "发现轻微划痕" + }, + "remark": "质检员补充" + } + ``` + +#### 3.8.2. 获取日志的参数 + +- **GET** `/api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/` +- **描述**: 获取某一次流转记录的所有参数。 +- **查询参数**: + - `key`: 如果提供,则只返回该 `key` 的所有历史值。 + - `include_cancelled`: `true` 或 `false`,是否包含已撤销的记录。 + + ## 分页 使用 LimitOffset 分页: diff --git a/stateflow/admin.py b/stateflow/admin.py index 6fbbc45..698ac97 100644 --- a/stateflow/admin.py +++ b/stateflow/admin.py @@ -214,11 +214,108 @@ class BusinessObjectAdmin(admin.ModelAdmin): return "-" +class StateLogParameterRecordInline(admin.TabularInline): + """状态流转参数记录内联显示""" + model = models.StateLogParameterRecord + extra = 0 + fields = ('parameters', 'remark', 'created_at') + readonly_fields = ('created_at',) + can_delete = False + + @admin.register(models.StateFlowRecord) class StateFlowRecordAdmin(admin.ModelAdmin): - list_display = ('id', 'business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at') + list_display = ('id', 'business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at', 'parameter_count') search_fields = ('business_object__name', 'state__name') list_filter = ('state', 'is_cancelled', 'completed_at', 'completed_by') - readonly_fields = ('business_object', 'state', 'completed_at', 'completed_by', 'cancelled_at') - fields = ('business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at') + readonly_fields = ('business_object', 'state', 'completed_at', 'completed_by', 'cancelled_at', 'parameters_summary') + fields = ('business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at', 'parameters_summary') autocomplete_fields = ('business_object', 'state', 'completed_by') + inlines = [StateLogParameterRecordInline] + + @admin.display(description='参数记录数') + def parameter_count(self, obj): + count = obj.parameter_records.count() + return f"{count} 条" if count > 0 else "-" + + @admin.display(description='参数汇总') + def parameters_summary(self, obj): + """显示所有参数的最新值汇总""" + summary = obj.get_all_parameters_summary(include_cancelled=True) + if summary: + items = [f"{k}: {v}" for k, v in list(summary.items())[:5]] + if len(summary) > 5: + items.append(f"... 共{len(summary)}个参数") + return mark_safe('
'.join(items)) + return "-" + + +@admin.register(models.ProcessNode) +class ProcessNodeAdmin(admin.ModelAdmin): + """流程节点关联管理""" + list_display = ('id', 'process', 'state', 'order', 'created_at') + search_fields = ('process__name', 'state__name') + list_filter = ('process', 'created_at') + autocomplete_fields = ('process', 'state') + list_editable = ('order',) + ordering = ('process', 'order') + + fieldsets = [ + ('关联信息', { + 'fields': ['process', 'state', 'order'] + }), + ] + + +@admin.register(models.StateLogParameterRecord) +class StateLogParameterRecordAdmin(admin.ModelAdmin): + """状态流转参数记录管理""" + list_display = ('id', 'state_log', 'parameter_keys', 'remark_preview', 'created_at') + search_fields = ('state_log__business_object__name', 'state_log__state__name', 'remark') + list_filter = ('created_at', 'state_log__state') + readonly_fields = ('created_at', 'parameter_preview') + autocomplete_fields = ('state_log',) + + fieldsets = [ + ('关联信息', { + 'fields': ['state_log'] + }), + ('参数数据', { + 'fields': ['parameters', 'parameter_preview', 'remark'] + }), + ('时间信息', { + 'fields': ['created_at'] + }), + ] + + @admin.display(description='参数键') + def parameter_keys(self, obj): + """显示参数键列表""" + keys = obj.get_all_keys() + if keys: + preview = ', '.join(keys[:5]) + if len(keys) > 5: + preview += f' 等{len(keys)}个' + return preview + return "-" + + @admin.display(description='备注') + def remark_preview(self, obj): + """显示备注预览""" + if obj.remark: + return obj.remark[:50] + ('...' if len(obj.remark) > 50 else '') + return "-" + + @admin.display(description='参数详情') + def parameter_preview(self, obj): + """格式化显示参数详情""" + if obj.parameters: + items = [] + for key, value in obj.parameters.items(): + # 如果值太长,截断显示 + value_str = str(value) + if len(value_str) > 100: + value_str = value_str[:100] + '...' + items.append(f"{key}: {value_str}") + return mark_safe('
'.join(items)) + return "-" diff --git a/stateflow/tests/test_services.py b/stateflow/tests/test_services.py index 0bbf23f..a9dd929 100644 --- a/stateflow/tests/test_services.py +++ b/stateflow/tests/test_services.py @@ -42,9 +42,9 @@ class StateFlowServicesTestCase(TestCase): ) def test_initial_state(self): - """测试初始状态 - 应该返回第一个待执行的节点""" + """测试初始状态 - 应该是 None(未开始)""" current_state = self.business_object.get_current_state() - self.assertEqual(current_state.name, self.state1.name, '初始状态应该返回第一个待执行的节点') + self.assertIsNone(current_state, '初始状态应该是 None(未开始)') self.assertEqual(self.business_object.get_progress_percentage(), 0.0) # 整体状态应该是 not_started @@ -57,18 +57,18 @@ class StateFlowServicesTestCase(TestCase): success, message, _ = services.advance_to_next_state(self.business_object, self.user) self.assertTrue(success) - # current_state 应该是下一个待执行的状态(state2) + # current_state 应该是最后完成的状态(state1) current_state = self.business_object.get_current_state() - self.assertEqual(current_state.name, self.state2.name) + 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 应该是 state3(下一个待执行的) + # current_state 应该是 state2 current_state = self.business_object.get_current_state() - self.assertEqual(current_state.name, self.state3.name) + self.assertEqual(current_state.name, self.state2.name) self.assertAlmostEqual(self.business_object.get_progress_percentage(), 66.67, places=1) # 完成最后一个状态(state3) @@ -76,9 +76,9 @@ class StateFlowServicesTestCase(TestCase): self.assertTrue(success) self.assertEqual(self.business_object.get_progress_percentage(), 100.0) - # current_state 应该是 None(没有待执行的节点了) + # current_state 应该是 state3(最后完成的状态) current_state = self.business_object.get_current_state() - self.assertIsNone(current_state) + self.assertEqual(current_state.name, self.state3.name) # 整体状态应该是 completed status = services.get_overall_status(self.business_object) @@ -116,7 +116,7 @@ class StateFlowServicesTestCase(TestCase): timeline = self.business_object.get_timeline() self.assertEqual(timeline[0]['status'], 'completed') - self.assertEqual(timeline[1]['status'], 'in_progress') # 下一个待执行的节点 + 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) @@ -141,9 +141,9 @@ class StateFlowServicesTestCase(TestCase): 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.assertEqual(current_state.name, self.state1.name) + self.assertIsNone(current_state) # 所有撤销的记录都应该有撤销时间 for log in self.business_object.state_logs.all():