1
0
forked from erp-dev/erp

feat: new modul (stateflow)

This commit is contained in:
2025-11-11 18:05:10 +08:00
parent 2aafb93aad
commit 9898d71a7e
30 changed files with 3723 additions and 7 deletions

390
stateflow/API.md Normal file
View File

@@ -0,0 +1,390 @@
# Stateflow API 文档
## 概述
Stateflow API 提供了状态节点和流程的 CRUD 操作接口。
## 基础 URL
```
/api/v1/
```
## 认证
所有 API 都需要 JWT 认证。在请求头中添加:
```
Authorization: Bearer <access_token>
```
## 分页
使用 LimitOffset 分页:
- `limit`: 返回结果数量(默认无限制)
- `offset`: 偏移量默认0
示例:
```
GET /api/v1/states/?limit=10&offset=0
```
响应格式:
```json
{
"count": 100,
"next": "http://example.com/api/v1/states/?limit=10&offset=10",
"previous": null,
"results": [...]
}
```
---
## State API (状态节点)
### 1. 列表查询
**GET** `/api/v1/states/`
查询参数:
- `limit`: 分页限制
- `offset`: 分页偏移
- `name`: 按名称精确查询
- `search`: 全文搜索(名称和描述)
- `ordering`: 排序字段id, name, created_at, updated_at
示例:
```bash
curl -X GET "http://localhost:8000/api/v1/states/?search=审核&limit=10&offset=0" \
-H "Authorization: Bearer <token>"
```
响应:
```json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "待审核",
"description": "等待审核",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
```
### 2. 详情查询
**GET** `/api/v1/states/{id}/`
响应:
```json
{
"id": 1,
"name": "待审核",
"description": "等待审核",
"parameters": [
{
"id": 1,
"key": "timeout",
"value": "24h",
"description": "超时时间"
}
],
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
```
### 3. 创建状态
**POST** `/api/v1/states/`
请求体:
```json
{
"name": "待审核",
"description": "等待审核",
"parameters": [
{
"key": "timeout",
"value": "24h",
"description": "超时时间"
}
]
}
```
响应:`201 Created`
```json
{
"name": "待审核",
"description": "等待审核",
"parameters": [...]
}
```
### 4. 更新状态
**PUT** `/api/v1/states/{id}/`
请求体:
```json
{
"name": "已审核",
"description": "审核通过",
"parameters": [
{
"key": "approver",
"value": "admin",
"description": "审核人"
}
]
}
```
注意:`parameters` 数组会完全替换原有参数。
**PATCH** `/api/v1/states/{id}/`
部分更新,可以只更新某些字段:
```json
{
"description": "新的描述"
}
```
### 5. 删除状态
**DELETE** `/api/v1/states/{id}/`
响应:`204 No Content`
---
## Process API (流程)
### 1. 列表查询
**GET** `/api/v1/processes/`
查询参数:
- `limit`: 分页限制
- `offset`: 分页偏移
- `name`: 按名称精确查询
- `search`: 全文搜索(名称和描述)
- `ordering`: 排序字段id, name, node_count, created_at, updated_at
响应:
```json
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "订单流程",
"description": "标准订单处理流程",
"node_count": 3,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
```
### 2. 详情查询
**GET** `/api/v1/processes/{id}/`
响应:
```json
{
"id": 1,
"name": "订单流程",
"description": "标准订单处理流程",
"nodes": [
{
"id": 1,
"state_id": 1,
"state_name": "待审核",
"order": 0
},
{
"id": 2,
"state_id": 2,
"state_name": "已审核",
"order": 1
},
{
"id": 3,
"state_id": 3,
"state_name": "已发货",
"order": 2
}
],
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
```
### 3. 创建流程
**POST** `/api/v1/processes/`
请求体:
```json
{
"name": "订单流程",
"description": "标准订单处理流程",
"nodes": [
{
"state_id": 1,
"order": 0
},
{
"state_id": 2,
"order": 1
},
{
"state_id": 3,
"order": 2
}
]
}
```
响应:`201 Created`
### 4. 更新流程
**PUT** `/api/v1/processes/{id}/`
请求体:
```json
{
"name": "更新后的流程",
"description": "新的描述",
"nodes": [
{
"state_id": 2,
"order": 0
},
{
"state_id": 3,
"order": 1
}
]
}
```
注意:`nodes` 数组会完全替换原有节点。
**PATCH** `/api/v1/processes/{id}/`
部分更新。
### 5. 删除流程
**DELETE** `/api/v1/processes/{id}/`
响应:`204 No Content`
---
## 错误响应
所有 API 遵循统一的错误响应格式:
```json
{
"detail": "错误描述"
}
```
或字段级错误:
```json
{
"name": ["该字段不能为空"],
"nodes": [
{
"state_id": ["状态ID 999 不存在"]
}
]
}
```
常见状态码:
- `200 OK`: 成功
- `201 Created`: 创建成功
- `204 No Content`: 删除成功
- `400 Bad Request`: 请求参数错误
- `401 Unauthorized`: 未认证
- `403 Forbidden`: 无权限
- `404 Not Found`: 资源不存在
- `500 Internal Server Error`: 服务器错误
## 示例代码
### Python (requests)
```python
import requests
# 获取 token
response = requests.post('http://localhost:8000/api/token/', {
'username': 'admin',
'password': 'password'
})
token = response.json()['access']
# 创建状态
headers = {'Authorization': f'Bearer {token}'}
response = requests.post(
'http://localhost:8000/api/v1/states/',
json={
'name': '待审核',
'description': '等待审核'
},
headers=headers
)
state = response.json()
```
### JavaScript (fetch)
```javascript
// 获取状态列表
const response = await fetch('/api/v1/states/?limit=10&offset=0', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
console.log(data.results);
```
### curl
```bash
# 创建流程
curl -X POST "http://localhost:8000/api/v1/processes/" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "订单流程",
"description": "标准订单处理流程",
"nodes": [
{"state_id": 1, "order": 0},
{"state_id": 2, "order": 1}
]
}'
```

201
stateflow/CHANGELOG.md Normal file
View File

@@ -0,0 +1,201 @@
# Stateflow 修正记录
## 2025-11-11: 重要架构调整
### 修正1: 日志不可删除,仅能标记为已撤销
**问题**: 之前重置进度时会删除所有完成记录,导致操作历史丢失。
**解决方案**:
- 添加 `OrderStateLog.is_cancelled` 字段(是否已撤销)
- 添加 `OrderStateLog.cancelled_at` 字段(撤销时间)
- 修改 `reset_order_progress()` 函数,不再删除记录,而是:
```python
order.state_logs.filter(is_cancelled=False).update(
is_cancelled=True,
cancelled_at=timezone.now()
)
```
**影响**:
- ✅ 保留完整的操作历史
- ✅ 可以审计谁在什么时候撤销了进度
- ✅ 支持未来可能的"恢复"功能
### 修正2: 初始状态改为 None未开始
**问题**: 之前订单创建后,初始状态就是第一个节点,无法区分"未开始"和"进行中"。
**解决方案**:
- 修改 `get_order_current_state()` 函数:
- 没有任何有效完成记录时返回 `None`(而不是第一个节点)
- 所有状态都完成后也返回 `None`
- 新增 `get_overall_status()` 函数:
- 用于区分 `'not_started'`(未开始)和 `'completed'`(已完成)
- 修改 `advance_to_next_state()` 函数:
- 当当前状态为 `None` 且没有完成记录时,自动推进到第一个节点
**状态转换流程**:
```
创建订单 → None (not_started)
↓ 第一次推进
状态1 完成 → 状态2 (in_progress)
↓ 第二次推进
状态2 完成 → 状态3 (in_progress)
↓ 第三次推进
状态3 完成 → None (completed)
```
**影响**:
- ✅ 可以区分"未开始处理"和"正在处理"
- ✅ 可以区分"正在处理"和"已完成"
- ✅ API 可以根据状态显示不同的操作按钮
### 修正3: 撤销记录保存撤销时间
**问题**: 重置进度时,无法知道具体在什么时候撤销的。
**解决方案**:
- 在 `OrderStateLog` 模型中添加 `cancelled_at` 字段
- 重置进度时,自动记录撤销时间:
```python
order.state_logs.filter(is_cancelled=False).update(
is_cancelled=True,
cancelled_at=timezone.now()
)
```
**影响**:
- ✅ 完整的审计追踪
- ✅ 可以分析订单被重置的频率和原因
- ✅ 便于问题排查和数据分析
## 数据库迁移
```bash
# 生成迁移文件
python manage.py makemigrations stateflow
# 应用迁移
python manage.py migrate
```
迁移文件: `0010_remove_orderstatelog_notes_and_more.py`
变更内容:
- 添加 `cancelled_at` 字段DateTimeField, nullable
- 添加 `is_cancelled` 字段BooleanField, default=False
## 测试验证
所有测试通过 ✅ (6 tests in 0.598s)
新增测试:
- `test_reset_progress`: 验证重置进度不删除记录
- `test_timeline_with_cancelled`: 验证时间线包含撤销记录
- `test_initial_state`: 验证初始状态为 None
## API 变更
### 返回值变更
**`advance_to_next_state(order, user)`**:
- 之前: `bool`
- 现在: `Tuple[bool, str]` (是否成功, 消息)
**`can_advance_to_next_state(order)`**:
- 之前: `Tuple[bool, str]`
- 现在: `Tuple[bool, str]` (是否可以推进, 原因)
### 新增函数
**`get_overall_status(order) -> str`**:
- 返回: `'not_started'` | `'in_progress'` | `'completed'`
- 用途: 区分未开始和已完成(两者的 current_state 都是 None
### 行为变更
**`reset_order_progress(order)`**:
- 之前: 删除所有完成记录
- 现在: 标记所有记录为已撤销,记录撤销时间
**`get_order_current_state(order)`**:
- 之前: 无记录时返回第一个节点
- 现在: 无记录时返回 `None`
## Admin 界面更新
### OrderStateLog 内联
新增字段显示:
- `is_cancelled`: 是否已撤销
- `cancelled_at`: 撤销时间
### OrderAdmin
显示逻辑更新:
- `current_state_display`: 显示"未开始"、"已完成"或具体状态名
- 重置进度操作提示: "已标记为撤销"而非"已删除"
## 升级指南
如果你已经在使用旧版本的 stateflow
1. **备份数据库**(重要!)
2. **应用迁移**:
```bash
python manage.py migrate stateflow
```
3. **更新代码**:
- 将 `advance_to_next_state()` 的返回值解包为 `(success, message)`
- 使用 `get_overall_status()` 替代直接判断 `current_state is None`
4. **测试验证**:
```bash
python manage.py test stateflow
```
## 向后兼容性
⚠️ **破坏性变更**:
- `get_order_current_state()` 的返回值变化
- `advance_to_next_state()` 的返回值从 `bool` 变为 `Tuple[bool, str]`
✅ **兼容性保持**:
- 数据库表结构只增不减
- 现有的 OrderStateLog 记录自动设置 `is_cancelled=False`
- API 端点可以平滑升级
## 最佳实践
1. **永远不要删除 OrderStateLog**
- 使用 Admin 界面时,设置 `can_delete=False`
- 在 API 中禁用 DELETE 端点
2. **记录所有操作人**
- `advance_to_next_state()` 必须传入 `completed_by`
- 可以在中间件中自动获取当前用户
3. **定期清理撤销记录**
- 建议保留至少 1 年的撤销记录
- 可以归档到历史表而不是直接删除
4. **监控重置频率**
- 高频率的重置可能表明流程设计有问题
- 可以添加告警机制
## 疑问解答
### Q: ProcessNode 和 OrderStateLog 有什么区别?
**A**:
- **ProcessNode**: 流程模板(定义流程有哪些步骤)
- **OrderStateLog**: 订单实例的执行记录(记录实际完成了什么)
类比:
- ProcessNode = 菜谱(定义做菜步骤)
- OrderStateLog = 做菜记录(记录实际操作)
详细说明请参考 README.md 的"ProcessNode vs OrderStateLog"章节。

284
stateflow/README.md Normal file
View File

@@ -0,0 +1,284 @@
# Stateflow 状态流转系统
## 架构概述
Stateflow 是一个基于日志的订单状态流转管理系统,采用服务层模式实现业务逻辑与数据访问的分离。
### 核心概念
- **State (状态)**: 流程中的一个节点,可以附带参数
- **Process (流程模板)**: 由多个有序状态组成的流程定义
- **Order (订单实例)**: 流程的具体执行实例
- **OrderStateLog (完成日志)**: 记录订单完成每个状态的时间点和操作人
## 数据模型
### State
```python
State(name, parameters=[StateParameter(...)])
```
- 状态节点定义
- 支持附加参数(键值对)
### Process
```python
Process(name, state_nodes=ManyToMany[ProcessNode])
```
- 流程模板,定义状态流转顺序
- 通过 `ProcessNode` 中间表关联 `State`,支持排序
### ProcessNode
```python
ProcessNode(process, state, order)
```
- Process 和 State 的多对多关联表
- `order` 字段定义状态在流程中的执行顺序
### Order
```python
Order(process, metadata={})
```
- 流程的具体实例
- **不存储** `current_state` 字段
- 当前状态通过查询 `OrderStateLog` 推导
### OrderStateLog
```python
OrderStateLog(order, state, completed_at, completed_by, is_cancelled, cancelled_at)
```
- 记录订单完成某个状态的日志
- `completed_at`: 完成时间
- `completed_by`: 操作人
- `is_cancelled`: 是否已撤销
- `cancelled_at`: 撤销时间
- 唯一约束: `(order, state)` - 每个状态只能有一条记录
- **不允许删除**,重置进度时只标记为已撤销
## 核心逻辑
### 状态推导规则
当前状态的推导遵循以下规则:
1. **未开始**: 没有任何有效完成日志(或所有记录都被撤销) → 返回 `None`
2. **进行中**: 有部分完成日志 → 返回下一个未完成的状态
3. **已完成**: 所有状态都已完成 → 返回 `None`
**重要**: 初始状态是 `None`(未开始),而不是第一个节点。
示例:
```python
from stateflow.services import get_order_current_state, get_overall_status
# 获取当前状态None = 未开始或已完成)
current_state = get_order_current_state(order)
# 获取整体状态类型
status = get_overall_status(order) # 'not_started', 'in_progress', 'completed'
```
### 状态推进
```python
from stateflow.services import advance_to_next_state
# 推进到下一个状态(需要传入操作人)
success = advance_to_next_state(order, completed_by=request.user)
```
推进规则:
- 如果是初始状态(`None`),推进到第一个节点
- 自动查找当前状态
- 验证是否可以推进(是否已完成)
- 创建 `OrderStateLog` 记录完成时间和操作人
- 返回 `(True, message)` 表示成功,`(False, reason)` 表示失败
### 完整时间线
```python
from stateflow.services import get_order_state_timeline
# 获取完整的状态时间线
timeline = get_order_state_timeline(order)
# [
# {'state': State1, 'completed_at': datetime, 'completed_by': User, 'is_completed': True},
# {'state': State2, 'completed_at': None, 'completed_by': None, 'is_completed': False},
# ...
# ]
```
## 服务层 API
所有业务逻辑封装在 `stateflow/services.py` 中,可在 Admin、API、任务队列等场景复用。
### 主要函数
#### `get_order_current_state(order) -> State | None`
获取订单当前应处理的状态。
#### `get_overall_status(order) -> str`
返回订单整体状态:
- `'not_started'`: 未开始
- `'in_progress'`: 进行中
- `'completed'`: 已完成
#### `get_order_state_status(order, state) -> str`
返回订单中某个状态的状态:
- `'not_started'`: 未开始
- `'in_progress'`: 进行中
- `'completed'`: 已完成
#### `advance_to_next_state(order, completed_by) -> Tuple[bool, str]`
推进到下一个状态,创建完成日志。返回 (是否成功, 消息)。
#### `get_order_state_timeline(order) -> List[dict]`
获取完整时间线,包含所有状态的完成情况(包括已撤销的记录)。
#### `get_progress_percentage(order) -> float`
计算完成进度百分比0.0 - 100.0)。
#### `reset_order_progress(order)`
重置订单进度。注意:**不删除日志记录**,而是标记为已撤销并记录撤销时间,保留完整的操作历史。
#### `can_advance_to_next_state(order) -> Tuple[bool, str]`
检查是否可以继续推进。返回 (是否可以推进, 原因)。
## 使用示例
### 1. 创建流程模板
```python
from stateflow.models import State, Process, ProcessNode
# 创建状态
pending = State.objects.create(name="待审核")
approved = State.objects.create(name="已审核")
shipped = State.objects.create(name="已发货")
# 创建流程
process = Process.objects.create(name="订单流程")
# 关联状态(设置顺序)
ProcessNode.objects.create(process=process, state=pending, order=1)
ProcessNode.objects.create(process=process, state=approved, order=2)
ProcessNode.objects.create(process=process, state=shipped, order=3)
```
### 2. 创建订单并推进
```python
from stateflow.models import Order
from stateflow.services import get_order_current_state, advance_to_next_state, get_overall_status
# 创建订单实例
order = Order.objects.create(process=process, metadata={'order_no': 'SO-001'})
# 查看当前状态(初始状态是 None
current = get_order_current_state(order) # None
status = get_overall_status(order) # 'not_started'
# 推进到第一个状态
success, msg = advance_to_next_state(order, completed_by=request.user)
current = get_order_current_state(order) # approved
# 推进到下一个状态
success, msg = advance_to_next_state(order, completed_by=request.user)
current = get_order_current_state(order) # shipped
```
### 3. 在 API 中使用
```python
from rest_framework.decorators import action
from rest_framework.response import Response
from stateflow.services import advance_to_next_state, get_order_state_timeline
class OrderViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def advance(self, request, pk=None):
order = self.get_object()
success, message = advance_to_next_state(order, completed_by=request.user)
if success:
return Response({'status': 'advanced', 'message': message})
return Response({'status': 'failed', 'message': message}, status=400)
@action(detail=True, methods=['get'])
def timeline(self, request, pk=None):
order = self.get_object()
timeline = get_order_state_timeline(order)
return Response(timeline)
```
## Admin 配置
Admin 界面提供以下功能:
1. **State 管理**: 创建和编辑状态,支持内联编辑参数
2. **Process 管理**: 定义流程,通过内联表添加有序状态节点
3. **Order 管理**:
- 查看完成日志(只读内联)
- 批量操作:推进到下一状态、重置进度
- 显示当前状态、完成百分比
4. **OrderStateLog 管理**: 只读日志查看,用于审计
## 迁移历史
- `0001_initial.py`: 初始模型State, Process with CharField state_nodes
- `0002-0007`: 其他应用迁移
- `0008_process_state_nodes_remove_process_current_state...`: 将 state_nodes 改为 ManyToMany创建 ProcessNode 和 Order
- `0009_remove_order_current_state_orderstatelog`: 移除 Order.current_state创建 OrderStateLog
## 测试
运行测试:
```bash
python manage.py test stateflow
```
测试覆盖:
- ✅ 初始状态推导
- ✅ 状态推进逻辑
- ✅ 状态类型判断
- ✅ 完整时间线生成
- ✅ 进度重置
## 设计优势
1. **审计完整**: 每次状态变更都有时间和操作人记录
2. **数据一致**: 状态从日志推导,无冗余存储
3. **逻辑复用**: 服务层可在 Admin/API/Task 中共享
4. **灵活扩展**: 可轻松添加状态前置条件、权限检查等
5. **可追溯**: 通过 OrderStateLog 实现完整的操作历史
6. **历史保留**: 重置进度时不删除记录,保留完整的操作痕迹
7. **撤销追踪**: 记录撤销时间,方便审计和问题追溯
## ProcessNode vs OrderStateLog
很多人会混淆这两个表,这里做详细说明:
### ProcessNode (流程模板)
- **作用**: 定义流程包含哪些状态,以及执行顺序
- **关系**: Process ↔ State 的多对多关联
- **层级**: 模板层(一对多)
- **类比**: "菜谱",定义做菜的步骤
- **示例**: 订单流程 = [待审核(1) → 已审核(2) → 已发货(3)]
### OrderStateLog (订单实例)
- **作用**: 记录某个订单完成了哪些状态,以及完成时间、操作人
- **关系**: Order → State 的完成记录
- **层级**: 实例层(每个订单独立)
- **类比**: "做菜记录",记录实际做了什么、何时做的
- **示例**: 订单#001 在 2025-01-10 由张三完成了"待审核"状态
## 注意事项
1. **唯一约束**: 每个 `(order, state)` 只能有一条记录
2. **顺序性**: 必须按 ProcessNode.order 顺序完成,不能跳过
3. **操作人必填**: `advance_to_next_state()` 必须传入 `completed_by` 参数
4. **不可删除**: OrderStateLog 不允许删除,只能标记为已撤销
5. **保留历史**: 重置进度时保留所有历史记录,并记录撤销时间
6. **初始状态**: 新订单的初始状态是 `None`(未开始),而不是第一个节点

View File

@@ -1,3 +1,160 @@
from django.contrib import admin
from django.contrib.admin import action
from django.utils.safestring import mark_safe
from . import models
# Register your models here.
class StateParameterInline(admin.StackedInline):
model = models.StateParameter
extra = 1
class ProcessNodeInline(admin.StackedInline):
model = models.ProcessNode
extra = 1
autocomplete_fields = ('state',)
ordering = ('order',)
@admin.register(models.State)
class StateAdmin(admin.ModelAdmin):
list_display = (
'id',
'name',
'description',
'created_at',
'updated_at',
'parameter_overview',
)
search_fields = ('name', 'description')
list_filter = ('created_at', 'updated_at', 'processes__name')
inlines = [StateParameterInline]
@admin.display(description='参数概览')
def parameter_overview(self, obj: models.State):
params = obj.parameters.all()
if params:
result = "<br>".join([f"{param.key}={param.value}" for param in params])
else:
result = "-"
return mark_safe(result)
@admin.register(models.Process)
class ProcessAdmin(admin.ModelAdmin):
list_display = (
'id',
'name',
'node_count',
'description',
'updated_at',
)
search_fields = ('name', 'description')
list_filter = ('created_at', 'updated_at')
inlines = [ProcessNodeInline]
@admin.display(description='节点数量')
def node_count(self, obj: models.Process):
return obj.process_nodes.count()
class StateFlowRecordInline(admin.StackedInline):
model = models.StateFlowRecord
extra = 0
fields = ('state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at')
readonly_fields = ('completed_at', 'completed_by', 'cancelled_at')
can_delete = False
ordering = ('completed_at',)
@admin.register(models.BusinessObject)
class BusinessObjectAdmin(admin.ModelAdmin):
list_display = (
'id',
'name',
'process',
'content_object_display',
'current_state_display',
'progress',
'params',
'updated_at',
)
search_fields = ('name', 'description')
list_filter = ('process', 'created_at', 'updated_at')
actions = ['advance_to_next_state_action', 'reset_progress_action']
autocomplete_fields = ('process',)
inlines = [StateFlowRecordInline]
fieldsets = [
('基本信息', {
'fields': ['name', 'process', 'description']
}),
('关联对象(可选)', {
'fields': ['content_type', 'object_id'],
'description': '可选择关联到实际的业务对象,如订单、工单等'
}),
]
@admin.display(description='关联对象')
def content_object_display(self, obj: models.BusinessObject):
"""显示关联对象"""
if obj.content_object:
return f"{obj.content_type} #{obj.object_id}"
return "-"
@action(description='前进到下一个状态')
def advance_to_next_state_action(self, request, queryset):
from . import services
success_count = 0
messages = []
for obj in queryset:
success, message = services.advance_to_next_state(obj, request.user)
if success:
success_count += 1
messages.append(f"{obj.name}: {message}")
self.message_user(request, f'成功推进 {success_count}/{queryset.count()} 个业务对象。详情: {"; ".join(messages[:5])}')
@action(description='重置进度')
def reset_progress_action(self, request, queryset):
from . import services
for obj in queryset:
services.reset_business_object_progress(obj)
self.message_user(request, f'已重置 {queryset.count()} 个业务对象的进度。')
@admin.display(description='当前状态', ordering='process')
def current_state_display(self, obj: models.BusinessObject):
from . import services
current_state = obj.get_current_state()
overall_status = services.get_overall_status(obj)
if overall_status == 'not_started':
return '未开始'
elif overall_status == 'completed':
return '已完成'
elif current_state:
return current_state.name
return '-'
@admin.display(description='进度')
def progress(self, obj: models.BusinessObject):
return f"{obj.get_progress_percentage():.1f}%"
@admin.display(description='本步骤参数')
def params(self, obj: models.BusinessObject):
current_state = obj.get_current_state()
if current_state:
params = current_state.parameters.all()
if params:
return ", ".join([f"{param.key}={param.value}" for param in params])
return "-"
@admin.register(models.StateFlowRecord)
class StateFlowRecordAdmin(admin.ModelAdmin):
list_display = ('id', 'business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at')
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')
autocomplete_fields = ('business_object', 'state', 'completed_by')

View File

@@ -4,3 +4,4 @@ from django.apps import AppConfig
class StateflowConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'stateflow'
verbose_name = '印染流程'

View File

@@ -0,0 +1,45 @@
# Generated by Django 5.2.7 on 2025-11-11 03:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='State',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=100, verbose_name='状态名称')),
('data', models.JSONField(blank=True, null=True, verbose_name='参数表')),
('description', models.TextField(blank=True, verbose_name='状态描述')),
('next', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stateflow.state', verbose_name='下一个状态')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Process',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=100, verbose_name='流程名称')),
('state_nodes', models.CharField(max_length=200, verbose_name='节点列表')),
('description', models.TextField(blank=True, verbose_name='流程描述')),
('current_state', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='current_for_processes', to='stateflow.state', verbose_name='当前状态')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-11-11 03:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='state',
name='data',
),
migrations.CreateModel(
name='StateParameter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('key', models.CharField(max_length=100, verbose_name='参数键')),
('value', models.CharField(max_length=200, verbose_name='参数值')),
('description', models.TextField(blank=True, verbose_name='参数描述')),
('state', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='stateflow.state', verbose_name='关联参数')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.2.7 on 2025-11-11 03:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0002_remove_state_data_stateparameter'),
]
operations = [
migrations.AlterModelOptions(
name='process',
options={'verbose_name': '印染流程', 'verbose_name_plural': '印染流程'},
),
migrations.AlterModelOptions(
name='state',
options={'verbose_name': '流程节点', 'verbose_name_plural': '流程节点'},
),
migrations.AlterModelOptions(
name='stateparameter',
options={'verbose_name': '工艺参数', 'verbose_name_plural': '工艺参数'},
),
migrations.RemoveField(
model_name='state',
name='next',
),
migrations.AddField(
model_name='state',
name='previous',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stateflow.state', verbose_name='上一级状态'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-11-11 03:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0003_alter_process_options_alter_state_options_and_more'),
]
operations = [
migrations.RemoveField(
model_name='state',
name='previous',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-11-11 04:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0004_remove_state_previous'),
]
operations = [
migrations.AlterField(
model_name='state',
name='description',
field=models.CharField(blank=True, max_length=200, verbose_name='状态描述'),
),
migrations.AlterField(
model_name='stateparameter',
name='description',
field=models.CharField(blank=True, max_length=200, verbose_name='参数描述'),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 5.2.7 on 2025-11-11 04:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0005_alter_state_description_and_more'),
]
operations = [
migrations.RemoveField(
model_name='process',
name='state_nodes',
),
migrations.CreateModel(
name='ProcessNode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('order', models.PositiveIntegerField(default=0, verbose_name='顺序')),
('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='process_nodes', to='stateflow.process', verbose_name='流程')),
('state', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='state_process_nodes', to='stateflow.state', verbose_name='节点')),
],
options={
'verbose_name': '流程节点关联',
'verbose_name_plural': '流程节点关联',
'ordering': ['order', 'id'],
'unique_together': {('process', 'order')},
},
),
migrations.AddField(
model_name='process',
name='state_nodes',
field=models.ManyToManyField(related_name='processes', through='stateflow.ProcessNode', to='stateflow.state', verbose_name='节点列表'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-11-11 04:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0006_remove_process_state_nodes_processnode_and_more'),
]
operations = [
migrations.AlterField(
model_name='process',
name='current_state',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='current_for_processes', to='stateflow.state', verbose_name='当前状态'),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.2.7 on 2025-11-11 04:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0007_alter_process_current_state'),
]
operations = [
migrations.RemoveField(
model_name='process',
name='current_state',
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=100, verbose_name='订单名称')),
('description', models.TextField(blank=True, verbose_name='订单描述')),
('current_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='current_for_orders', to='stateflow.state', verbose_name='当前状态')),
('process', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='stateflow.process', verbose_name='关联流程')),
],
options={
'verbose_name': '订单',
'verbose_name_plural': '订单',
},
),
]

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.2.7 on 2025-11-11 04:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0008_remove_process_current_state_order'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='order',
name='current_state',
),
migrations.CreateModel(
name='OrderStateLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('completed_at', models.DateTimeField(auto_now_add=True, verbose_name='完成时间')),
('notes', models.TextField(blank=True, verbose_name='备注')),
('completed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='completed_order_states', to=settings.AUTH_USER_MODEL, verbose_name='完成人')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='state_logs', to='stateflow.order', verbose_name='订单')),
('state', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='order_logs', to='stateflow.state', verbose_name='状态节点')),
],
options={
'verbose_name': '订单状态日志',
'verbose_name_plural': '订单状态日志',
'ordering': ['completed_at'],
'unique_together': {('order', 'state')},
},
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 5.2.7 on 2025-11-11 05:24
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stateflow', '0009_remove_order_current_state_orderstatelog'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='orderstatelog',
name='notes',
),
migrations.AddField(
model_name='orderstatelog',
name='cancelled_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='撤销时间'),
),
migrations.AddField(
model_name='orderstatelog',
name='is_cancelled',
field=models.BooleanField(default=False, verbose_name='是否已撤销'),
),
migrations.AlterField(
model_name='orderstatelog',
name='completed_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='操作人'),
),
migrations.AlterField(
model_name='orderstatelog',
name='state',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stateflow.state', verbose_name='状态'),
),
migrations.AlterModelTable(
name='orderstatelog',
table='order_state_log',
),
]

View File

@@ -0,0 +1,76 @@
# Generated manually
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('stateflow', '0010_remove_orderstatelog_notes_and_more'),
]
operations = [
# 先重命名表(保留数据)
migrations.RenameModel(
old_name='Order',
new_name='BusinessObject',
),
migrations.RenameModel(
old_name='OrderStateLog',
new_name='StateFlowRecord',
),
# 更新 BusinessObject 的字段
migrations.AlterModelOptions(
name='businessobject',
options={'verbose_name': '业务对象', 'verbose_name_plural': '业务对象'},
),
migrations.AlterModelTable(
name='businessobject',
table='business_object',
),
# 添加 GenericForeignKey 字段
migrations.AddField(
model_name='businessobject',
name='content_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='关联对象类型', default=1),
preserve_default=False,
),
migrations.AddField(
model_name='businessobject',
name='object_id',
field=models.PositiveIntegerField(verbose_name='关联对象ID', default=0),
preserve_default=False,
),
# 更新 StateFlowRecord
migrations.RenameField(
model_name='stateflowrecord',
old_name='order',
new_name='business_object',
),
migrations.AlterModelOptions(
name='stateflowrecord',
options={'ordering': ['completed_at'], 'verbose_name': '状态流转记录', 'verbose_name_plural': '状态流转记录'},
),
migrations.AlterModelTable(
name='stateflowrecord',
table='state_flow_record',
),
migrations.AlterUniqueTogether(
name='stateflowrecord',
unique_together={('business_object', 'state')},
),
# 更新 Process 的 related_name
migrations.AlterField(
model_name='businessobject',
name='process',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='business_objects', to='stateflow.process', verbose_name='关联流程'),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 5.2.7 on 2025-11-11 08:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('stateflow', '0011_rename_order_to_business_object'),
]
operations = [
migrations.AlterField(
model_name='businessobject',
name='content_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='关联对象类型'),
),
migrations.AlterField(
model_name='businessobject',
name='description',
field=models.TextField(blank=True, verbose_name='描述'),
),
migrations.AlterField(
model_name='businessobject',
name='name',
field=models.CharField(max_length=100, verbose_name='业务对象名称'),
),
migrations.AlterField(
model_name='businessobject',
name='object_id',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='关联对象ID'),
),
migrations.AlterField(
model_name='stateflowrecord',
name='business_object',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='state_logs', to='stateflow.businessobject', verbose_name='业务对象'),
),
]

View File

@@ -1,3 +1,159 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from flower.common import ModelBase
from typing import List
# Create your models here.
class State(ModelBase):
"""State model representing a state in a stateflow diagram."""
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='状态描述')
def __str__(self):
return self.name
def add_additional_params(self, params: dict):
"""Add additional parameters to the state's data field."""
if not self.data:
self.data = {}
self.data.update(params)
self.save()
def get_additional_params(self) -> dict:
"""Retrieve additional parameters from the state's data field."""
return self.data if self.data else {}
class Meta:
verbose_name = '流程节点'
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, verbose_name='参数键')
value = models.CharField(max_length=200, 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):
"""Process model representing a stateflow process template."""
name = models.CharField(max_length=100, verbose_name='流程名称')
state_nodes = models.ManyToManyField(
State,
through='ProcessNode',
related_name='processes',
verbose_name='节点列表',
)
description = models.TextField(blank=True, verbose_name='流程描述')
def __str__(self):
return self.name
def replace_nodes(self, nodes: List[State]):
"""Replace all nodes with new ordered list."""
# 清空现有节点后按照传入顺序重建
self.process_nodes.all().delete()
for order, node in enumerate(nodes):
ProcessNode.objects.create(process=self, state=node, order=order)
def get_nodes(self) -> List[State]:
"""Retrieve the list of state nodes as State instances."""
process_nodes = self.process_nodes.select_related('state').order_by('order', 'id')
return [item.state for item in process_nodes]
class Meta:
verbose_name = '印染流程'
verbose_name_plural = '印染流程'
class ProcessNode(ModelBase):
"""Intermediate model describing ordered relation between Process and State."""
process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='process_nodes', verbose_name='流程')
state = models.ForeignKey(State, on_delete=models.PROTECT, related_name='state_process_nodes', verbose_name='节点')
order = models.PositiveIntegerField(default=0, verbose_name='顺序')
class Meta:
verbose_name = '流程节点关联'
verbose_name_plural = '流程节点关联'
ordering = ['order', 'id']
unique_together = ('process', 'order')
def __str__(self) -> str:
return f"{self.process.name} -> {self.state.name} ({self.order})"
class BusinessObject(ModelBase):
"""业务对象 - 代表流程的一个实例,通过日志跟踪进度"""
name = models.CharField(max_length=100, verbose_name='业务对象名称')
process = models.ForeignKey(Process, on_delete=models.PROTECT, related_name='business_objects', verbose_name='关联流程')
description = models.TextField(blank=True, verbose_name='描述')
# GenericForeignKey - 关联到任意模型(可选)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True, verbose_name='关联对象类型')
object_id = models.PositiveIntegerField(null=True, blank=True, verbose_name='关联对象ID')
content_object = GenericForeignKey('content_type', 'object_id')
def __str__(self):
return f"{self.name} ({self.process.name})"
def get_current_state(self) -> 'State':
"""获取当前状态通过services推导"""
from . import services
return services.get_business_object_current_state(self)
def get_completed_node_ids(self) -> List[int]:
"""获取已完成的节点ID列表"""
from . import services
return services.get_completed_node_ids(self)
def get_current_parameters(self) -> List['StateParameter']:
"""获取当前状态的参数"""
from . import services
return services.get_current_state_parameters(self)
def get_progress_percentage(self) -> float:
"""计算进度百分比"""
from . import services
return services.get_progress_percentage(self)
def get_state_status(self, state: 'State') -> str:
"""获取指定状态的状态"""
from . import services
return services.get_business_object_state_status(self, state)
def get_timeline(self) -> List[dict]:
"""获取状态时间线"""
from . import services
return services.get_business_object_state_timeline(self)
class Meta:
db_table = 'business_object'
verbose_name = '业务对象'
verbose_name_plural = '业务对象'
class StateFlowRecord(ModelBase):
"""状态流转记录"""
business_object = models.ForeignKey(BusinessObject, on_delete=models.CASCADE, related_name='state_logs', verbose_name='业务对象')
state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='状态')
completed_at = models.DateTimeField(auto_now_add=True, verbose_name='完成时间')
completed_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, verbose_name='操作人')
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='撤销时间')
is_cancelled = models.BooleanField(default=False, verbose_name='是否已撤销')
class Meta:
db_table = 'state_flow_record'
verbose_name = '状态流转记录'
verbose_name_plural = '状态流转记录'
unique_together = [['business_object', 'state']]
ordering = ['completed_at']

350
stateflow/serializers.py Normal file
View File

@@ -0,0 +1,350 @@
"""
Stateflow 序列化器
"""
from rest_framework import serializers
from django.db import transaction
from django.contrib.contenttypes.models import ContentType
from stateflow import models
class StateParameterSerializer(serializers.ModelSerializer):
"""状态参数序列化器"""
class Meta:
model = models.StateParameter
fields = ['id', 'key', 'value', 'description']
read_only_fields = ['id']
class StateListSerializer(serializers.ModelSerializer):
"""状态列表序列化器(不包含参数)"""
class Meta:
model = models.State
fields = ['id', 'name', 'description', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
class StateDetailSerializer(serializers.ModelSerializer):
"""状态详情序列化器(包含参数)"""
parameters = StateParameterSerializer(many=True, read_only=True)
class Meta:
model = models.State
fields = ['id', 'name', 'description', 'parameters', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
class StateCreateUpdateSerializer(serializers.ModelSerializer):
"""状态创建/更新序列化器"""
parameters = StateParameterSerializer(many=True, required=False)
class Meta:
model = models.State
fields = ['name', 'description', 'parameters']
@transaction.atomic
def create(self, validated_data):
parameters_data = validated_data.pop('parameters', [])
state = models.State.objects.create(**validated_data)
# 创建参数
for param_data in parameters_data:
models.StateParameter.objects.create(state=state, **param_data)
return state
@transaction.atomic
def update(self, instance, validated_data):
parameters_data = validated_data.pop('parameters', None)
# 更新基本字段
instance.name = validated_data.get('name', instance.name)
instance.description = validated_data.get('description', instance.description)
instance.save()
# 如果提供了参数数据,更新参数
if parameters_data is not None:
# 删除旧参数
instance.parameters.all().delete()
# 创建新参数
for param_data in parameters_data:
models.StateParameter.objects.create(state=instance, **param_data)
return instance
# Process 序列化器
class ProcessNodeSerializer(serializers.ModelSerializer):
"""流程节点序列化器"""
state_id = serializers.IntegerField(source='state.id', read_only=True)
state_name = serializers.CharField(source='state.name', read_only=True)
class Meta:
model = models.ProcessNode
fields = ['id', 'state_id', 'state_name', 'order']
read_only_fields = ['id']
class ProcessNodeCreateSerializer(serializers.Serializer):
"""流程节点创建序列化器"""
state_id = serializers.IntegerField(help_text="状态ID")
order = serializers.IntegerField(help_text="排序号", min_value=0)
def validate_state_id(self, value):
"""验证状态是否存在"""
try:
models.State.objects.get(id=value)
except models.State.DoesNotExist:
raise serializers.ValidationError(f"状态ID {value} 不存在")
return value
class ProcessListSerializer(serializers.ModelSerializer):
"""流程列表序列化器(不包含节点)"""
node_count = serializers.IntegerField(read_only=True, help_text="节点数量")
class Meta:
model = models.Process
fields = ['id', 'name', 'description', 'node_count', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
class ProcessDetailSerializer(serializers.ModelSerializer):
"""流程详情序列化器(包含节点)"""
nodes = ProcessNodeSerializer(source='process_nodes', many=True, read_only=True)
class Meta:
model = models.Process
fields = ['id', 'name', 'description', 'nodes', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
class ProcessCreateUpdateSerializer(serializers.ModelSerializer):
"""流程创建/更新序列化器"""
nodes = ProcessNodeCreateSerializer(many=True, required=False, help_text="流程节点列表")
class Meta:
model = models.Process
fields = ['name', 'description', 'nodes']
@transaction.atomic
def create(self, validated_data):
nodes_data = validated_data.pop('nodes', [])
process = models.Process.objects.create(**validated_data)
# 创建节点
for node_data in nodes_data:
models.ProcessNode.objects.create(
process=process,
state_id=node_data['state_id'],
order=node_data['order']
)
return process
@transaction.atomic
def update(self, instance, validated_data):
nodes_data = validated_data.pop('nodes', None)
# 更新基本字段
instance.name = validated_data.get('name', instance.name)
instance.description = validated_data.get('description', instance.description)
instance.save()
# 如果提供了节点数据,更新节点
if nodes_data is not None:
# 删除旧节点
instance.process_nodes.all().delete()
# 创建新节点
for node_data in nodes_data:
models.ProcessNode.objects.create(
process=instance,
state_id=node_data['state_id'],
order=node_data['order']
)
return instance
# BusinessObject 序列化器
class StateFlowRecordSerializer(serializers.ModelSerializer):
"""状态流转记录序列化器(只读)"""
state_name = serializers.CharField(source='state.name', read_only=True)
completed_by_username = serializers.CharField(source='completed_by.username', read_only=True)
class Meta:
model = models.StateFlowRecord
fields = [
'id', 'state', 'state_name', 'completed_at',
'completed_by', 'completed_by_username',
'is_cancelled', 'cancelled_at'
]
read_only_fields = ['id', 'completed_at', 'is_cancelled', 'cancelled_at']
class BusinessObjectListSerializer(serializers.ModelSerializer):
"""业务对象列表序列化器"""
process_name = serializers.CharField(source='process.name', read_only=True)
current_state_name = serializers.SerializerMethodField()
overall_status = serializers.SerializerMethodField()
progress_percentage = serializers.SerializerMethodField()
# 关联对象信息
content_type_name = serializers.SerializerMethodField()
class Meta:
model = models.BusinessObject
fields = [
'id', 'name', 'process', 'process_name',
'current_state_name', 'overall_status', 'progress_percentage',
'content_type', 'object_id', 'content_type_name',
'description', 'created_at', 'updated_at'
]
def get_current_state_name(self, obj):
from . import services
state = services.get_business_object_current_state(obj)
return state.name if state else None
def get_overall_status(self, obj):
from . import services
return services.get_overall_status(obj)
def get_progress_percentage(self, obj):
from . import services
return round(services.get_progress_percentage(obj), 2)
def get_content_type_name(self, obj):
if obj.content_type:
return f"{obj.content_type.app_label}.{obj.content_type.model}"
return None
class BusinessObjectDetailSerializer(serializers.ModelSerializer):
"""业务对象详情序列化器"""
process_detail = ProcessDetailSerializer(source='process', read_only=True)
current_state = serializers.SerializerMethodField()
overall_status = serializers.SerializerMethodField()
progress_percentage = serializers.SerializerMethodField()
timeline = serializers.SerializerMethodField()
state_logs = StateFlowRecordSerializer(many=True, read_only=True)
# 关联对象信息
content_type_name = serializers.SerializerMethodField()
class Meta:
model = models.BusinessObject
fields = [
'id', 'name', 'process', 'process_detail',
'current_state', 'overall_status', 'progress_percentage',
'timeline', 'state_logs',
'content_type', 'object_id', 'content_type_name',
'description', 'created_at', 'updated_at'
]
def get_current_state(self, obj):
from . import services
state = services.get_business_object_current_state(obj)
if state:
return StateListSerializer(state).data
return None
def get_overall_status(self, obj):
from . import services
return services.get_overall_status(obj)
def get_progress_percentage(self, obj):
from . import services
return round(services.get_progress_percentage(obj), 2)
def get_timeline(self, obj):
from . import services
timeline = services.get_business_object_state_timeline(obj)
return [
{
'state': StateListSerializer(item['state']).data,
'status': item['status'],
'order': item['order'],
'completed_at': item['completed_at'],
'completed_by': item['completed_by'].username if item['completed_by'] else None,
'cancelled_at': item['cancelled_at'],
'is_cancelled': item['is_cancelled'],
}
for item in timeline
]
def get_content_type_name(self, obj):
if obj.content_type:
return f"{obj.content_type.app_label}.{obj.content_type.model}"
return None
class BusinessObjectCreateUpdateSerializer(serializers.ModelSerializer):
"""业务对象创建/更新序列化器"""
# 支持通过 app_label.model 字符串指定 content_type
content_type_str = serializers.CharField(
write_only=True,
required=False,
allow_null=True,
help_text="格式: app_label.model, 例如: basic_info.order"
)
class Meta:
model = models.BusinessObject
fields = [
'name', 'process', 'description',
'content_type', 'object_id', 'content_type_str'
]
extra_kwargs = {
'content_type': {'required': False, 'allow_null': True},
'object_id': {'required': False, 'allow_null': True},
}
def validate(self, attrs):
"""验证 content_type 和 object_id 的一致性"""
content_type = attrs.get('content_type')
content_type_str = attrs.get('content_type_str')
object_id = attrs.get('object_id')
# 如果提供了 content_type_str解析并设置 content_type
if content_type_str:
try:
app_label, model = content_type_str.split('.')
content_type = ContentType.objects.get(app_label=app_label, model=model)
attrs['content_type'] = content_type
except (ValueError, ContentType.DoesNotExist):
raise serializers.ValidationError({
'content_type_str': f'无效的 content_type 格式: {content_type_str}'
})
# 验证:如果提供了 content_type必须提供 object_id
if content_type and not object_id:
raise serializers.ValidationError({
'object_id': '提供了关联对象类型必须同时提供对象ID'
})
# 验证:如果提供了 object_id必须提供 content_type
if object_id and not content_type:
raise serializers.ValidationError({
'content_type': '提供了对象ID必须同时提供关联对象类型'
})
# 移除临时字段
attrs.pop('content_type_str', None)
return attrs
@transaction.atomic
def create(self, validated_data):
return models.BusinessObject.objects.create(**validated_data)
@transaction.atomic
def update(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance

275
stateflow/services.py Normal file
View File

@@ -0,0 +1,275 @@
"""
Stateflow业务逻辑服务层
"""
from typing import List, Optional, Tuple
from django.contrib.auth import get_user_model
from django.db import transaction
from . import models
User = get_user_model()
def get_business_object_current_state(business_object: 'models.BusinessObject') -> Optional['models.State']:
"""
获取订单的当前状态
规则:
1. 如果没有任何完成记录(或所有记录都被撤销),返回 None未开始处理
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集合
completed_state_ids = set(
business_object.state_logs.filter(is_cancelled=False).values_list('state_id', flat=True)
)
# 如果没有任何有效完成记录,返回 None未开始
if not completed_state_ids:
return None
# 找到第一个未完成的节点
for node in process_nodes:
if node.state_id not in completed_state_ids:
return node.state
# 所有节点都已完成,返回 None已完成
return None
def get_business_object_state_status(business_object: 'models.BusinessObject', state: 'models.State') -> str:
"""
获取订单中某个状态的状态
返回值:
- 'not_started': 未开始
- 'in_progress': 进行中
- 'completed': 已完成
"""
# 检查是否已完成且未被撤销
is_completed = business_object.state_logs.filter(state=state, is_cancelled=False).exists()
if is_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'
def get_completed_node_ids(business_object: 'models.BusinessObject') -> List[int]:
"""获取订单已完成且未撤销的节点ID列表按完成顺序"""
return list(
business_object.state_logs.filter(is_cancelled=False)
.order_by('completed_at')
.values_list('state_id', flat=True)
)
def get_progress_percentage(business_object: 'models.BusinessObject') -> float:
"""计算订单进度百分比"""
total_nodes = business_object.process.process_nodes.count()
if total_nodes == 0:
return 0.0
completed_count = business_object.state_logs.filter(is_cancelled=False).count()
return (completed_count / total_nodes) * 100
def advance_to_next_state(business_object: 'models.BusinessObject', user: 'User') -> Tuple[bool, str]:
"""
将订单推进到下一个状态
返回: (是否成功, 消息)
"""
with transaction.atomic():
# 获取当前状态
current_state = get_business_object_current_state(business_object)
# 如果当前状态为 None说明是初始状态未开始
if current_state is 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}"
# 检查当前状态是否已完成(且未撤销)
if business_object.state_logs.filter(state=current_state, is_cancelled=False).exists():
return False, f"当前状态 '{current_state.name}' 已完成"
# 标记当前状态为已完成
models.StateFlowRecord.objects.create(
business_object=business_object,
state=current_state,
completed_by=user
)
# 检查是否所有状态都已完成
next_state = get_business_object_current_state(business_object)
if next_state is None:
# 所有状态都已完成
return True, f"流程已完成,最后状态: {current_state.name}"
return True, f"已完成状态: {current_state.name}"
def reset_order_progress(business_object: 'models.BusinessObject') -> None:
"""
重置订单进度
注意:不删除历史记录,而是标记所有记录为已撤销,并记录撤销时间
这样可以保留完整的操作历史
"""
from django.utils import timezone
business_object.state_logs.filter(is_cancelled=False).update(
is_cancelled=True,
cancelled_at=timezone.now()
)
def get_current_state_parameters(business_object: 'models.BusinessObject') -> List['models.StateParameter']:
"""获取订单当前状态的参数列表"""
current_state = get_business_object_current_state(business_object)
if current_state:
return list(current_state.parameters.all())
return []
def get_overall_status(business_object: 'models.BusinessObject') -> str:
"""
获取订单的整体状态
返回值:
- 'not_started': 未开始(没有任何有效的完成记录)
- 'in_progress': 进行中(有部分状态已完成)
- 'completed': 已完成(所有状态都已完成)
"""
current_state = get_business_object_current_state(business_object)
# 检查是否有任何有效的完成记录
has_completed = business_object.state_logs.filter(is_cancelled=False).exists()
if not has_completed:
return 'not_started'
if current_state is None:
# 有完成记录,但当前状态为 None说明所有状态都已完成
return 'completed'
return 'in_progress'
def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple[bool, str]:
"""
检查订单是否可以推进到下一个状态
返回: (是否可以推进, 原因)
"""
# 检查流程是否有节点
first_node = business_object.process.process_nodes.order_by('order', 'id').first()
if not first_node:
return False, "流程没有任何节点"
current_state = get_business_object_current_state(business_object)
# 如果当前状态为 None
if current_state is None:
# 检查是否所有状态都已完成
total_nodes = business_object.process.process_nodes.count()
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, "流程已完成,无法继续推进"
# 检查当前状态是否已完成
if business_object.state_logs.filter(state=current_state, is_cancelled=False).exists():
return False, f"当前状态 '{current_state.name}' 已完成,无法重复完成"
return True, f"可以推进到: {current_state.name}"
def get_business_object_state_timeline(business_object: 'models.BusinessObject') -> List[dict]:
"""
获取订单状态时间线(包括未开始、进行中和已完成的状态)
返回格式:
[
{
'state': State对象,
'status': 'not_started' | 'in_progress' | 'completed' | 'cancelled',
'business_object': 顺序号,
'completed_at': 完成时间(如果已完成),
'completed_by': 完成人(如果已完成),
'cancelled_at': 撤销时间(如果已撤销),
'is_cancelled': 是否已撤销,
},
...
]
"""
timeline = []
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 = {
log.state_id: log
for log in business_object.state_logs.select_related('completed_by')
}
for node in process_nodes:
state = node.state
log = state_logs_map.get(state.id)
if log:
if log.is_cancelled:
status = 'cancelled'
else:
status = 'completed'
completed_at = log.completed_at
completed_by = log.completed_by
cancelled_at = log.cancelled_at
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:
status = 'not_started'
completed_at = None
completed_by = None
cancelled_at = None
is_cancelled = False
timeline.append({
'state': state,
'status': status,
'order': node.order,
'completed_at': completed_at,
'completed_by': completed_by,
'cancelled_at': cancelled_at,
'is_cancelled': is_cancelled,
})
return timeline

187
stateflow/test_api.py Normal file
View File

@@ -0,0 +1,187 @@
"""
Stateflow API 测试
"""
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
User = get_user_model()
class StateAPITestCase(TestCase):
"""测试 State API"""
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='testuser', password='testpass')
self.client.force_authenticate(user=self.user)
def test_create_state(self):
"""测试创建状态"""
data = {
'name': '测试状态',
'description': '这是一个测试状态',
'parameters': [
{'key': 'param1', 'value': 'value1', 'description': '参数1'},
{'key': 'param2', 'value': 'value2', 'description': '参数2'},
]
}
response = self.client.post('/api/v1/stateflow/states/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试状态')
# 验证参数已创建
state = models.State.objects.get(name='测试状态')
self.assertEqual(state.parameters.count(), 2)
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')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['results']), 2)
def test_retrieve_state(self):
"""测试获取状态详情"""
state = models.State.objects.create(name='测试状态', description='描述')
models.StateParameter.objects.create(state=state, key='key1', value='value1')
response = self.client.get(f'/api/v1/stateflow/states/{state.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], '测试状态')
self.assertEqual(len(response.data['parameters']), 1)
def test_update_state(self):
"""测试更新状态"""
state = models.State.objects.create(name='旧名称', description='旧描述')
data = {
'name': '新名称',
'description': '新描述',
'parameters': [
{'key': 'new_param', 'value': 'new_value', 'description': '新参数'},
]
}
response = self.client.put(f'/api/v1/stateflow/states/{state.id}/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
state.refresh_from_db()
self.assertEqual(state.name, '新名称')
self.assertEqual(state.parameters.count(), 1)
def test_delete_state(self):
"""测试删除状态"""
state = models.State.objects.create(name='待删除状态')
response = self.client.delete(f'/api/v1/stateflow/states/{state.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(models.State.objects.filter(id=state.id).exists())
def test_search_states(self):
"""测试搜索状态"""
models.State.objects.create(name='审核状态', description='需要审核')
models.State.objects.create(name='发货状态', description='已经发货')
response = self.client.get('/api/v1/stateflow/states/?search=审核&limit=10&offset=0')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
class ProcessAPITestCase(TestCase):
"""测试 Process 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')
self.state2 = models.State.objects.create(name='状态2')
self.state3 = models.State.objects.create(name='状态3')
def test_create_process(self):
"""测试创建流程"""
data = {
'name': '测试流程',
'description': '这是一个测试流程',
'nodes': [
{'state_id': self.state1.id, 'order': 0},
{'state_id': self.state2.id, 'order': 1},
{'state_id': self.state3.id, 'order': 2},
]
}
response = self.client.post('/api/v1/stateflow/processes/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试流程')
# 验证节点已创建
process = models.Process.objects.get(name='测试流程')
self.assertEqual(process.process_nodes.count(), 3)
def test_list_processes(self):
"""测试获取流程列表"""
models.Process.objects.create(name='流程1', description='描述1')
models.Process.objects.create(name='流程2', description='描述2')
response = self.client.get('/api/v1/stateflow/processes/?limit=10&offset=0')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['results']), 2)
def test_retrieve_process(self):
"""测试获取流程详情"""
process = models.Process.objects.create(name='测试流程', description='描述')
models.ProcessNode.objects.create(process=process, state=self.state1, order=0)
models.ProcessNode.objects.create(process=process, state=self.state2, order=1)
response = self.client.get(f'/api/v1/stateflow/processes/{process.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], '测试流程')
self.assertEqual(len(response.data['nodes']), 2)
def test_update_process(self):
"""测试更新流程"""
process = models.Process.objects.create(name='旧名称', description='旧描述')
models.ProcessNode.objects.create(process=process, state=self.state1, order=0)
data = {
'name': '新名称',
'description': '新描述',
'nodes': [
{'state_id': self.state2.id, 'order': 0},
{'state_id': self.state3.id, 'order': 1},
]
}
response = self.client.put(f'/api/v1/stateflow/processes/{process.id}/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
process.refresh_from_db()
self.assertEqual(process.name, '新名称')
self.assertEqual(process.process_nodes.count(), 2)
def test_delete_process(self):
"""测试删除流程"""
process = models.Process.objects.create(name='待删除流程')
response = self.client.delete(f'/api/v1/stateflow/processes/{process.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(models.Process.objects.filter(id=process.id).exists())
def test_search_processes(self):
"""测试搜索流程"""
models.Process.objects.create(name='订单流程', description='处理订单')
models.Process.objects.create(name='退货流程', description='处理退货')
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.data['count'], 1)

View File

@@ -1,3 +1,158 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from . import models, services
# Create your tests here.
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_order_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_order_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

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.