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

View File

@@ -0,0 +1,5 @@
from .state import StateViewSet
from .process import ProcessViewSet
from .business_object import BusinessObjectViewSet
__all__ = ['StateViewSet', 'ProcessViewSet', 'BusinessObjectViewSet']

View File

@@ -0,0 +1,169 @@
"""
BusinessObject API ViewSet
"""
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import LimitOffsetPagination
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as django_filters
from stateflow import models, services
from stateflow.serializers import (
BusinessObjectListSerializer,
BusinessObjectDetailSerializer,
BusinessObjectCreateUpdateSerializer,
StateListSerializer,
)
class BusinessObjectFilterSet(django_filters.FilterSet):
"""业务对象过滤器"""
name = django_filters.CharFilter(lookup_expr='icontains')
process_name = django_filters.CharFilter(field_name='process__name', lookup_expr='icontains')
overall_status = django_filters.ChoiceFilter(
choices=[
('not_started', '未开始'),
('in_progress', '进行中'),
('completed', '已完成')
],
method='filter_overall_status'
)
content_type_str = django_filters.CharFilter(method='filter_content_type')
has_content_object = django_filters.BooleanFilter(method='filter_has_content_object')
def filter_overall_status(self, queryset, name, value):
"""过滤整体状态"""
business_object_ids = []
for obj in queryset:
if services.get_overall_status(obj) == value:
business_object_ids.append(obj.id)
return queryset.filter(id__in=business_object_ids)
def filter_content_type(self, queryset, name, value):
"""通过 app_label.model 字符串过滤"""
try:
from django.contrib.contenttypes.models import ContentType
app_label, model = value.split('.')
content_type = ContentType.objects.get(app_label=app_label, model=model)
return queryset.filter(content_type=content_type)
except:
return queryset.none()
def filter_has_content_object(self, queryset, name, value):
"""过滤是否有关联对象"""
if value:
return queryset.exclude(content_type__isnull=True)
else:
return queryset.filter(content_type__isnull=True)
class Meta:
model = models.BusinessObject
fields = ['name', 'process', 'process_name', 'overall_status',
'content_type_str', 'has_content_object']
class BusinessObjectViewSet(viewsets.ModelViewSet):
"""
业务对象 CRUD 接口
业务对象是流程的实例,可以选择性地关联到实际的业务模型(如订单、工单等)
list: 获取业务对象列表
retrieve: 获取业务对象详情(包含时间线和日志)
create: 创建业务对象
update: 更新业务对象
partial_update: 部分更新业务对象
destroy: 删除业务对象
查询参数:
- name: 按名称模糊查询
- process: 按流程ID过滤
- process_name: 按流程名称模糊查询
- overall_status: 按整体状态过滤 (not_started/in_progress/completed)
- content_type_str: 按关联对象类型过滤,格式: app_label.model
- has_content_object: 是否有关联对象 (true/false)
- search: 全文搜索(名称和描述)
- ordering: 排序字段
"""
queryset = models.BusinessObject.objects.all()
pagination_class = LimitOffsetPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = BusinessObjectFilterSet
search_fields = ['name', 'description']
ordering_fields = ['id', 'name', 'created_at', 'updated_at']
ordering = ['-created_at']
def get_serializer_class(self):
"""根据动作选择序列化器"""
if self.action == 'list':
return BusinessObjectListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return BusinessObjectCreateUpdateSerializer
else: # retrieve
return BusinessObjectDetailSerializer
def get_queryset(self):
"""优化查询"""
queryset = super().get_queryset()
if self.action == 'list':
queryset = queryset.select_related('process', 'content_type')
elif self.action == 'retrieve':
queryset = queryset.select_related('process', 'content_type').prefetch_related(
'state_logs__state',
'state_logs__completed_by',
'process__process_nodes__state'
)
return queryset
@action(detail=True, methods=['post'])
def advance(self, request, pk=None):
"""推进到下一个状态"""
business_object = self.get_object()
user = request.user
success, message = services.advance_to_next_state(business_object, user)
if success:
return Response({
'success': True,
'message': message,
'business_object': BusinessObjectDetailSerializer(business_object).data
})
else:
return Response({
'success': False,
'message': message
}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=True, methods=['post'])
def reset(self, request, pk=None):
"""重置进度"""
business_object = self.get_object()
services.reset_order_progress(business_object)
return Response({
'success': True,
'message': '进度已重置',
'business_object': BusinessObjectDetailSerializer(business_object).data
})
@action(detail=True, methods=['get'])
def timeline(self, request, pk=None):
"""获取状态时间线"""
business_object = self.get_object()
timeline = services.get_business_object_state_timeline(business_object)
timeline_data = [
{
'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
]
return Response(timeline_data)

View File

@@ -0,0 +1,59 @@
"""
Process API ViewSet
"""
from rest_framework import viewsets, filters
from rest_framework.pagination import LimitOffsetPagination
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count
from stateflow import models
from stateflow.serializers import (
ProcessListSerializer,
ProcessDetailSerializer,
ProcessCreateUpdateSerializer,
)
class ProcessViewSet(viewsets.ModelViewSet):
"""
流程 CRUD 接口
list: 获取流程列表
retrieve: 获取流程详情(包含节点)
create: 创建流程
update: 更新流程
partial_update: 部分更新流程
destroy: 删除流程
查询参数:
- name: 按名称模糊查询
- search: 全文搜索(名称和描述)
- ordering: 排序字段,支持 id, name, node_count, created_at, updated_at
"""
pagination_class = LimitOffsetPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['name']
search_fields = ['name', 'description']
ordering_fields = ['id', 'name', 'node_count', 'created_at', 'updated_at']
ordering = ['-created_at']
def get_serializer_class(self):
"""根据动作选择序列化器"""
if self.action == 'list':
return ProcessListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return ProcessCreateUpdateSerializer
else: # retrieve
return ProcessDetailSerializer
def get_queryset(self):
"""优化查询"""
queryset = models.Process.objects.all()
if self.action == 'list':
# list 时添加节点计数注解
queryset = queryset.annotate(node_count=Count('process_nodes'))
elif self.action == 'retrieve':
# retrieve 时预加载节点和状态
queryset = queryset.prefetch_related('process_nodes__state')
return queryset

View File

@@ -0,0 +1,53 @@
"""
State API ViewSet
"""
from rest_framework import viewsets, filters
from rest_framework.pagination import LimitOffsetPagination
from django_filters.rest_framework import DjangoFilterBackend
from stateflow import models
from stateflow.serializers import (
StateListSerializer,
StateDetailSerializer,
StateCreateUpdateSerializer,
)
class StateViewSet(viewsets.ModelViewSet):
"""
状态节点 CRUD 接口
list: 获取状态列表
retrieve: 获取状态详情(包含参数)
create: 创建状态
update: 更新状态
partial_update: 部分更新状态
destroy: 删除状态
查询参数:
- name: 按名称模糊查询
- search: 全文搜索(名称和描述)
- ordering: 排序字段,支持 id, name, created_at, updated_at
"""
queryset = models.State.objects.all()
pagination_class = LimitOffsetPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['name']
search_fields = ['name', 'description']
ordering_fields = ['id', 'name', 'created_at', 'updated_at']
ordering = ['-created_at']
def get_serializer_class(self):
"""根据动作选择序列化器"""
if self.action == 'list':
return StateListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return StateCreateUpdateSerializer
else: # retrieve
return StateDetailSerializer
def get_queryset(self):
"""优化查询retrieve 时预加载参数"""
queryset = super().get_queryset()
if self.action == 'retrieve':
queryset = queryset.prefetch_related('parameters')
return queryset

View File

@@ -0,0 +1,819 @@
openapi: 3.0.3
info:
title: Flower Stateflow API
version: 1.0.0
description: >
REST API for managing Stateflow states, processes, and business objects.
servers:
- url: http://localhost:8000/api/v1
security:
- BearerAuth: []
tags:
- name: States
- name: Processes
- name: BusinessObjects
paths:
/stateflow/states/:
get:
tags: [States]
summary: List states
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- name: name
in: query
schema:
type: string
description: Filter by exact name
- name: search
in: query
schema:
type: string
description: Full-text search across name and description
- name: ordering
in: query
schema:
type: string
enum: [id, -id, name, -name, created_at, -created_at, updated_at, -updated_at]
description: Order results by a field (prefix with "-" for descending)
responses:
'200':
description: Paginated state list
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedStateList'
post:
tags: [States]
summary: Create state
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StateCreateRequest'
responses:
'201':
description: Created state
content:
application/json:
schema:
$ref: '#/components/schemas/StateDetail'
/stateflow/states/{id}/:
parameters:
- $ref: '#/components/parameters/StateId'
get:
tags: [States]
summary: Retrieve state
responses:
'200':
description: State detail
content:
application/json:
schema:
$ref: '#/components/schemas/StateDetail'
put:
tags: [States]
summary: Update state
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StateCreateRequest'
responses:
'200':
description: Updated state
content:
application/json:
schema:
$ref: '#/components/schemas/StateDetail'
patch:
tags: [States]
summary: Partially update state
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StatePartialRequest'
responses:
'200':
description: Updated state
content:
application/json:
schema:
$ref: '#/components/schemas/StateDetail'
delete:
tags: [States]
summary: Delete state
responses:
'204':
description: Deleted
/stateflow/processes/:
get:
tags: [Processes]
summary: List processes
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- name: name
in: query
schema:
type: string
description: Filter by exact name
- name: search
in: query
schema:
type: string
description: Full-text search across name and description
- name: ordering
in: query
schema:
type: string
enum: [id, -id, name, -name, node_count, -node_count, created_at, -created_at, updated_at, -updated_at]
description: Order results by a field (prefix with "-" for descending)
responses:
'200':
description: Paginated process list
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedProcessList'
post:
tags: [Processes]
summary: Create process
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessCreateRequest'
responses:
'201':
description: Created process
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDetail'
/stateflow/processes/{id}/:
parameters:
- $ref: '#/components/parameters/ProcessId'
get:
tags: [Processes]
summary: Retrieve process
responses:
'200':
description: Process detail
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDetail'
put:
tags: [Processes]
summary: Update process
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessCreateRequest'
responses:
'200':
description: Updated process
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDetail'
patch:
tags: [Processes]
summary: Partially update process
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessPartialRequest'
responses:
'200':
description: Updated process
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDetail'
delete:
tags: [Processes]
summary: Delete process
responses:
'204':
description: Deleted
/stateflow/business-objects/:
get:
tags: [BusinessObjects]
summary: List business objects
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- name: name
in: query
schema:
type: string
description: Filter by name (case-insensitive, contains)
- name: process
in: query
schema:
type: integer
description: Filter by process ID
- name: process_name
in: query
schema:
type: string
description: Filter by process name (case-insensitive, contains)
- name: overall_status
in: query
schema:
type: string
enum: [not_started, in_progress, completed]
description: Filter by overall status
- name: content_type_str
in: query
schema:
type: string
description: Filter by related object type (app_label.model)
- name: has_content_object
in: query
schema:
type: boolean
description: true for only records linked to actual objects; false for unlinked
- name: search
in: query
schema:
type: string
description: Full-text search across name and description
- name: ordering
in: query
schema:
type: string
enum: [id, -id, name, -name, created_at, -created_at, updated_at, -updated_at]
description: Order results by a field (prefix with "-" for descending)
responses:
'200':
description: Paginated business object list
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedBusinessObjectList'
post:
tags: [BusinessObjects]
summary: Create business object
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectCreateRequest'
responses:
'201':
description: Created business object
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectDetail'
/stateflow/business-objects/{id}/:
parameters:
- $ref: '#/components/parameters/BusinessObjectId'
get:
tags: [BusinessObjects]
summary: Retrieve business object
responses:
'200':
description: Business object detail
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectDetail'
put:
tags: [BusinessObjects]
summary: Update business object
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectCreateRequest'
responses:
'200':
description: Updated business object
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectDetail'
patch:
tags: [BusinessObjects]
summary: Partially update business object
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectPartialRequest'
responses:
'200':
description: Updated business object
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectDetail'
delete:
tags: [BusinessObjects]
summary: Delete business object
responses:
'204':
description: Deleted
/stateflow/business-objects/{id}/advance/:
parameters:
- $ref: '#/components/parameters/BusinessObjectId'
post:
tags: [BusinessObjects]
summary: Advance business object to next state
responses:
'200':
description: Advanced successfully
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectAdvanceResponse'
'400':
description: Cannot advance
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectAdvanceResponse'
/stateflow/business-objects/{id}/reset/:
parameters:
- $ref: '#/components/parameters/BusinessObjectId'
post:
tags: [BusinessObjects]
summary: Reset progress (mark records as cancelled)
responses:
'200':
description: Reset successful
content:
application/json:
schema:
$ref: '#/components/schemas/BusinessObjectResetResponse'
/stateflow/business-objects/{id}/timeline/:
parameters:
- $ref: '#/components/parameters/BusinessObjectId'
get:
tags: [BusinessObjects]
summary: Get state timeline
responses:
'200':
description: Timeline data
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/StateTimelineItem'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
Limit:
name: limit
in: query
schema:
type: integer
minimum: 0
description: Number of results to return per page (default configured on server)
Offset:
name: offset
in: query
schema:
type: integer
minimum: 0
description: Offset into the result set for pagination
StateId:
name: id
in: path
required: true
schema:
type: integer
description: State ID
ProcessId:
name: id
in: path
required: true
schema:
type: integer
description: Process ID
BusinessObjectId:
name: id
in: path
required: true
schema:
type: integer
description: Business object ID
schemas:
DateTime:
type: string
format: date-time
StateParameter:
type: object
properties:
id:
type: integer
key:
type: string
value:
type: string
description:
type: string
nullable: true
required: [id, key, value]
StateListItem:
type: object
properties:
id:
type: integer
name:
type: string
description:
type: string
nullable: true
created_at:
$ref: '#/components/schemas/DateTime'
updated_at:
$ref: '#/components/schemas/DateTime'
required: [id, name, created_at, updated_at]
StateDetail:
allOf:
- $ref: '#/components/schemas/StateListItem'
- type: object
properties:
parameters:
type: array
items:
$ref: '#/components/schemas/StateParameter'
StateCreateRequest:
type: object
properties:
name:
type: string
description:
type: string
nullable: true
parameters:
type: array
items:
type: object
properties:
key:
type: string
value:
type: string
description:
type: string
nullable: true
required: [key, value]
required: [name]
StatePartialRequest:
type: object
properties:
name:
type: string
description:
type: string
nullable: true
parameters:
type: array
nullable: true
items:
type: object
properties:
key:
type: string
value:
type: string
description:
type: string
nullable: true
required: [key, value]
ProcessNode:
type: object
properties:
id:
type: integer
state_id:
type: integer
state_name:
type: string
order:
type: integer
required: [id, state_id, state_name, order]
ProcessListItem:
type: object
properties:
id:
type: integer
name:
type: string
description:
type: string
nullable: true
node_count:
type: integer
created_at:
$ref: '#/components/schemas/DateTime'
updated_at:
$ref: '#/components/schemas/DateTime'
required: [id, name, node_count, created_at, updated_at]
ProcessDetail:
allOf:
- $ref: '#/components/schemas/ProcessListItem'
- type: object
properties:
nodes:
type: array
items:
$ref: '#/components/schemas/ProcessNode'
ProcessCreateRequest:
type: object
properties:
name:
type: string
description:
type: string
nullable: true
nodes:
type: array
items:
type: object
properties:
state_id:
type: integer
order:
type: integer
required: [state_id, order]
required: [name]
ProcessPartialRequest:
type: object
properties:
name:
type: string
description:
type: string
nullable: true
nodes:
type: array
nullable: true
items:
type: object
properties:
state_id:
type: integer
order:
type: integer
required: [state_id, order]
StateFlowRecord:
type: object
properties:
id:
type: integer
state:
type: integer
state_name:
type: string
completed_at:
$ref: '#/components/schemas/DateTime'
nullable: true
completed_by:
type: integer
nullable: true
completed_by_username:
type: string
nullable: true
is_cancelled:
type: boolean
cancelled_at:
$ref: '#/components/schemas/DateTime'
nullable: true
required: [id, state, state_name, is_cancelled]
StateTimelineItem:
type: object
properties:
state:
$ref: '#/components/schemas/StateListItem'
status:
type: string
enum: [not_started, in_progress, completed, cancelled]
order:
type: integer
completed_at:
$ref: '#/components/schemas/DateTime'
nullable: true
completed_by:
type: string
nullable: true
cancelled_at:
$ref: '#/components/schemas/DateTime'
nullable: true
is_cancelled:
type: boolean
required: [state, status, order, is_cancelled]
BusinessObjectListItem:
type: object
properties:
id:
type: integer
name:
type: string
process:
type: integer
process_name:
type: string
current_state_name:
type: string
nullable: true
overall_status:
type: string
enum: [not_started, in_progress, completed]
progress_percentage:
type: number
format: float
content_type:
type: integer
nullable: true
object_id:
type: integer
nullable: true
content_type_name:
type: string
nullable: true
description:
type: string
nullable: true
created_at:
$ref: '#/components/schemas/DateTime'
updated_at:
$ref: '#/components/schemas/DateTime'
required: [id, name, process, process_name, overall_status, progress_percentage, created_at, updated_at]
BusinessObjectDetail:
allOf:
- $ref: '#/components/schemas/BusinessObjectListItem'
- type: object
properties:
process_detail:
$ref: '#/components/schemas/ProcessDetail'
current_state:
$ref: '#/components/schemas/StateDetail'
nullable: true
timeline:
type: array
items:
$ref: '#/components/schemas/StateTimelineItem'
state_logs:
type: array
items:
$ref: '#/components/schemas/StateFlowRecord'
BusinessObjectCreateRequest:
type: object
properties:
name:
type: string
process:
type: integer
description:
type: string
nullable: true
content_type:
type: integer
nullable: true
object_id:
type: integer
nullable: true
content_type_str:
type: string
nullable: true
description: Alternative to content_type; format app_label.model
required: [name, process]
BusinessObjectPartialRequest:
type: object
properties:
name:
type: string
process:
type: integer
description:
type: string
nullable: true
content_type:
type: integer
nullable: true
object_id:
type: integer
nullable: true
content_type_str:
type: string
nullable: true
BusinessObjectAdvanceResponse:
type: object
properties:
success:
type: boolean
message:
type: string
business_object:
$ref: '#/components/schemas/BusinessObjectDetail'
nullable: true
required: [success, message]
BusinessObjectResetResponse:
type: object
properties:
success:
type: boolean
message:
type: string
business_object:
$ref: '#/components/schemas/BusinessObjectDetail'
required: [success, message, business_object]
PaginatedStateList:
type: object
properties:
count:
type: integer
next:
type: string
nullable: true
previous:
type: string
nullable: true
results:
type: array
items:
$ref: '#/components/schemas/StateListItem'
required: [count, results]
PaginatedProcessList:
type: object
properties:
count:
type: integer
next:
type: string
nullable: true
previous:
type: string
nullable: true
results:
type: array
items:
$ref: '#/components/schemas/ProcessListItem'
required: [count, results]
PaginatedBusinessObjectList:
type: object
properties:
count:
type: integer
next:
type: string
nullable: true
previous:
type: string
nullable: true
results:
type: array
items:
$ref: '#/components/schemas/BusinessObjectListItem'
required: [count, results]