forked from erp-dev/erp
309 lines
10 KiB
Python
309 lines
10 KiB
Python
import logging
|
|
|
|
from django.db import transaction
|
|
from django.utils import timezone
|
|
|
|
from mission.models import Mission, MissionCategory, MissionParticipant, MissionReply
|
|
from mission.signals import (
|
|
mission_cancelled,
|
|
mission_completed,
|
|
mission_created,
|
|
mission_reopened,
|
|
mission_replied,
|
|
mission_reply_rejected,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_MISSION_CATEGORY_NAME = "通用"
|
|
|
|
|
|
def _send_signal_on_commit(signal, *, sender, **payload) -> None:
|
|
def _send():
|
|
try:
|
|
signal.send(sender=sender, **payload)
|
|
except Exception:
|
|
logger.exception("[mission.services] 触发 mission signal 失败(已忽略)")
|
|
|
|
transaction.on_commit(_send)
|
|
|
|
|
|
def _assert_employee_belongs_to_mission(employee, mission: Mission, role: str) -> None:
|
|
if employee is None:
|
|
raise ValueError(f"{role}不能为空")
|
|
if employee.merchant_id != mission.merchant_id:
|
|
raise ValueError(f"{role}不属于任务所属商户")
|
|
|
|
|
|
def _get_default_mission_category(*, merchant) -> MissionCategory:
|
|
category, _ = MissionCategory.objects.get_or_create(
|
|
merchant=merchant,
|
|
name=DEFAULT_MISSION_CATEGORY_NAME,
|
|
)
|
|
return category
|
|
|
|
|
|
def _assert_category_belongs_to_merchant(category: MissionCategory | None, *, merchant) -> None:
|
|
if category is None:
|
|
return
|
|
if category.merchant_id != merchant.id:
|
|
raise ValueError("任务分类不属于当前商户")
|
|
|
|
|
|
def _validate_content_object_merchant(*, content_type, content_id, merchant) -> None:
|
|
if content_type is None and content_id is None:
|
|
return
|
|
if content_type is None or content_id is None:
|
|
raise ValueError("content_type 与 content_id 必须同时提供或同时为空")
|
|
|
|
try:
|
|
content_object = content_type.get_object_for_this_type(pk=content_id)
|
|
except Exception as exc:
|
|
raise ValueError("关联业务对象不存在") from exc
|
|
|
|
content_object_merchant_id = getattr(content_object, "merchant_id", None)
|
|
if content_object_merchant_id is not None and content_object_merchant_id != merchant.id:
|
|
raise ValueError("关联业务对象不属于当前商户")
|
|
|
|
|
|
@transaction.atomic
|
|
def set_mission_participants(*, mission: Mission, participant_ids: list[int]) -> Mission:
|
|
mission = Mission.objects.select_for_update().get(pk=mission.pk)
|
|
participant_ids = list(dict.fromkeys(participant_ids or []))
|
|
|
|
from basic_info.models import Employee
|
|
|
|
employees = list(Employee.objects.filter(id__in=participant_ids, merchant=mission.merchant))
|
|
if len(employees) != len(participant_ids):
|
|
raise ValueError("参与者不存在或不属于任务所属商户")
|
|
|
|
MissionParticipant.objects.filter(mission=mission).exclude(employee_id__in=participant_ids).delete()
|
|
existing_employee_ids = set(
|
|
MissionParticipant.objects.filter(mission=mission, employee_id__in=participant_ids)
|
|
.values_list("employee_id", flat=True)
|
|
)
|
|
MissionParticipant.objects.bulk_create(
|
|
[
|
|
MissionParticipant(merchant=mission.merchant, mission=mission, employee=employee)
|
|
for employee in employees
|
|
if employee.id not in existing_employee_ids
|
|
]
|
|
)
|
|
return mission
|
|
|
|
|
|
@transaction.atomic
|
|
def create_mission(
|
|
*,
|
|
creator,
|
|
description: str,
|
|
category: MissionCategory | None = None,
|
|
content_type=None,
|
|
content_id: int | None = None,
|
|
participant_ids: list[int] | None = None,
|
|
) -> Mission:
|
|
if creator is None:
|
|
raise ValueError("任务创建者不能为空")
|
|
merchant = creator.merchant
|
|
_validate_content_object_merchant(content_type=content_type, content_id=content_id, merchant=merchant)
|
|
_assert_category_belongs_to_merchant(category, merchant=merchant)
|
|
|
|
mission = Mission.objects.create(
|
|
merchant=merchant,
|
|
creator=creator,
|
|
description=description,
|
|
category=category or _get_default_mission_category(merchant=merchant),
|
|
content_type=content_type,
|
|
content_id=content_id,
|
|
)
|
|
if participant_ids is not None:
|
|
set_mission_participants(mission=mission, participant_ids=participant_ids)
|
|
_send_signal_on_commit(
|
|
mission_created,
|
|
sender=Mission,
|
|
instance=mission,
|
|
created_by=creator,
|
|
)
|
|
return mission
|
|
|
|
|
|
@transaction.atomic
|
|
def update_mission(
|
|
*,
|
|
mission: Mission,
|
|
updated_by,
|
|
description: str | None = None,
|
|
category: MissionCategory | None = None,
|
|
content_type=None,
|
|
content_id: int | None = None,
|
|
update_content_object: bool = False,
|
|
participant_ids: list[int] | None = None,
|
|
) -> Mission:
|
|
mission = Mission.objects.select_for_update().get(pk=mission.pk)
|
|
_assert_employee_belongs_to_mission(updated_by, mission, "更新任务的员工")
|
|
|
|
update_fields = []
|
|
if description is not None:
|
|
mission.description = description
|
|
update_fields.append("description")
|
|
if category is not None:
|
|
_assert_category_belongs_to_merchant(category, merchant=mission.merchant)
|
|
mission.category = category
|
|
update_fields.append("category")
|
|
if update_content_object:
|
|
_validate_content_object_merchant(
|
|
content_type=content_type,
|
|
content_id=content_id,
|
|
merchant=mission.merchant,
|
|
)
|
|
mission.content_type = content_type
|
|
mission.content_id = content_id
|
|
update_fields.extend(["content_type", "content_id"])
|
|
|
|
if update_fields:
|
|
mission.save(update_fields=[*update_fields, "updated_at"])
|
|
if participant_ids is not None:
|
|
set_mission_participants(mission=mission, participant_ids=participant_ids)
|
|
return mission
|
|
|
|
|
|
@transaction.atomic
|
|
def create_mission_reply(
|
|
*,
|
|
mission: Mission,
|
|
responder,
|
|
content: str,
|
|
ends_task: bool = False,
|
|
) -> MissionReply:
|
|
mission = Mission.objects.select_for_update().get(pk=mission.pk)
|
|
_assert_employee_belongs_to_mission(responder, mission, "回应者")
|
|
if not mission.can_reply:
|
|
raise ValueError("任务当前不允许继续回应")
|
|
|
|
reply = MissionReply.objects.create(
|
|
merchant=mission.merchant,
|
|
mission=mission,
|
|
responder=responder,
|
|
content=content,
|
|
ends_task=ends_task,
|
|
)
|
|
if ends_task and not mission.is_completed:
|
|
mission.is_completed = True
|
|
mission.save(update_fields=["is_completed", "updated_at"])
|
|
_send_signal_on_commit(
|
|
mission_completed,
|
|
sender=Mission,
|
|
instance=mission,
|
|
completed_by=responder,
|
|
reply=reply,
|
|
)
|
|
_send_signal_on_commit(
|
|
mission_replied,
|
|
sender=MissionReply,
|
|
instance=reply,
|
|
mission=mission,
|
|
responder=responder,
|
|
)
|
|
return reply
|
|
|
|
|
|
@transaction.atomic
|
|
def reopen_mission(*, mission: Mission, reopened_by) -> Mission:
|
|
mission = Mission.objects.select_for_update().get(pk=mission.pk)
|
|
_assert_employee_belongs_to_mission(reopened_by, mission, "重新打开任务的员工")
|
|
ending_replies = mission.replies.select_for_update().filter(ends_task=True, is_rejected=False)
|
|
if mission.is_cancelled:
|
|
raise ValueError("已取消任务不能重新打开")
|
|
if not mission.is_completed:
|
|
raise ValueError("未完成任务不能重新打开")
|
|
if not ending_replies.exists():
|
|
raise ValueError("任务没有可撤销的结束回应")
|
|
|
|
rejected_reply_ids = list(ending_replies.values_list("id", flat=True))
|
|
now = timezone.now()
|
|
ending_replies.update(ends_task=False, is_rejected=True, rejected_by=reopened_by, rejected_at=now)
|
|
mission.is_completed = False
|
|
mission.save(update_fields=["is_completed", "updated_at"])
|
|
for reply in MissionReply.objects.filter(id__in=rejected_reply_ids):
|
|
_send_signal_on_commit(
|
|
mission_reply_rejected,
|
|
sender=MissionReply,
|
|
instance=reply,
|
|
mission=mission,
|
|
rejected_by=reopened_by,
|
|
reason="reopen",
|
|
)
|
|
_send_signal_on_commit(
|
|
mission_reopened,
|
|
sender=Mission,
|
|
instance=mission,
|
|
reopened_by=reopened_by,
|
|
rejected_reply_ids=rejected_reply_ids,
|
|
)
|
|
return mission
|
|
|
|
|
|
@transaction.atomic
|
|
def reject_reply(*, reply: MissionReply, rejected_by) -> MissionReply:
|
|
reply = MissionReply.objects.select_for_update().select_related("mission").get(pk=reply.pk)
|
|
mission = Mission.objects.select_for_update().get(pk=reply.mission_id)
|
|
_assert_employee_belongs_to_mission(rejected_by, mission, "撤销回应的员工")
|
|
|
|
if mission.is_cancelled:
|
|
raise ValueError("已取消任务不能撤销回应")
|
|
if reply.is_rejected:
|
|
raise ValueError("回应已被撤销")
|
|
|
|
was_ending_reply = reply.ends_task
|
|
now = timezone.now()
|
|
reply.ends_task = False
|
|
reply.is_rejected = True
|
|
reply.rejected_by = rejected_by
|
|
reply.rejected_at = now
|
|
reply.save(update_fields=["ends_task", "is_rejected", "rejected_by", "rejected_at", "updated_at"])
|
|
|
|
has_other_ending_reply = mission.replies.exclude(pk=reply.pk).filter(ends_task=True, is_rejected=False).exists()
|
|
if was_ending_reply and not has_other_ending_reply and mission.is_completed:
|
|
mission.is_completed = False
|
|
mission.save(update_fields=["is_completed", "updated_at"])
|
|
_send_signal_on_commit(
|
|
mission_reply_rejected,
|
|
sender=MissionReply,
|
|
instance=reply,
|
|
mission=mission,
|
|
rejected_by=rejected_by,
|
|
reason="reject_reply",
|
|
)
|
|
return reply
|
|
|
|
|
|
@transaction.atomic
|
|
def cancel_mission(*, mission: Mission, cancelled_by) -> Mission:
|
|
mission = Mission.objects.select_for_update().get(pk=mission.pk)
|
|
_assert_employee_belongs_to_mission(cancelled_by, mission, "取消任务的员工")
|
|
|
|
if mission.is_cancelled:
|
|
raise ValueError("任务已取消")
|
|
|
|
mission.is_cancelled = True
|
|
mission.cancelled_by = cancelled_by
|
|
mission.cancelled_at = timezone.now()
|
|
mission.save(update_fields=["is_cancelled", "cancelled_by", "cancelled_at", "updated_at"])
|
|
_send_signal_on_commit(
|
|
mission_cancelled,
|
|
sender=Mission,
|
|
instance=mission,
|
|
cancelled_by=cancelled_by,
|
|
)
|
|
return mission
|
|
|
|
|
|
@transaction.atomic
|
|
def set_mission_urgent(*, mission: Mission, updated_by, is_urgent: bool) -> Mission:
|
|
mission = Mission.objects.select_for_update().get(pk=mission.pk)
|
|
_assert_employee_belongs_to_mission(updated_by, mission, "更新任务紧急状态的员工")
|
|
|
|
mission.is_urgent = is_urgent
|
|
mission.save(update_fields=["is_urgent", "updated_at"])
|
|
return mission
|