1
0
forked from erp-dev/erp

feat: mission replied calling task

This commit is contained in:
2026-04-17 23:26:33 +08:00
parent e109c00837
commit 8b1afc64c5
22 changed files with 1056 additions and 95 deletions

View File

@@ -1,4 +1,5 @@
import logging
from datetime import timedelta
from django.db import transaction
from django.utils import timezone
@@ -16,6 +17,7 @@ from mission.signals import (
logger = logging.getLogger(__name__)
DEFAULT_MISSION_CATEGORY_NAME = "通用"
UNSET = object()
def _send_signal_on_commit(signal, *, sender, **payload) -> None:
@@ -66,6 +68,41 @@ def _validate_content_object_merchant(*, content_type, content_id, merchant) ->
raise ValueError("关联业务对象不属于当前商户")
def _validate_unreplied_notification_config(
*,
notify_if_unreplied: bool,
unreplied_notify_interval_minutes: int | None,
unreplied_notify_max_count: int,
) -> None:
if notify_if_unreplied and unreplied_notify_interval_minutes is None:
raise ValueError("开启未回复提醒时必须设置提醒间隔")
if unreplied_notify_interval_minutes is not None and unreplied_notify_interval_minutes <= 0:
raise ValueError("未回复提醒间隔必须大于 0")
if unreplied_notify_max_count <= 0:
raise ValueError("未回复最大提醒次数必须大于 0")
def _reset_unreplied_notification_state(mission: Mission) -> None:
mission.unreplied_notify_sent_count = 0
mission.unreplied_last_notified_at = None
def _has_effective_reply(*, mission: Mission) -> bool:
return mission.replies.filter(is_rejected=False).exists()
def _clear_unreplied_notification_state_if_active(*, mission: Mission) -> None:
if mission.unreplied_notify_sent_count or mission.unreplied_last_notified_at is not None:
_reset_unreplied_notification_state(mission)
mission.save(
update_fields=[
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
"updated_at",
]
)
@transaction.atomic
def set_mission_participants(*, mission: Mission, participant_ids: list[int]) -> Mission:
mission = Mission.objects.select_for_update().get(pk=mission.pk)
@@ -101,12 +138,20 @@ def create_mission(
content_type=None,
content_id: int | None = None,
participant_ids: list[int] | None = None,
notify_if_unreplied: bool = False,
unreplied_notify_interval_minutes: int | None = None,
unreplied_notify_max_count: int = 5,
) -> 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)
_validate_unreplied_notification_config(
notify_if_unreplied=notify_if_unreplied,
unreplied_notify_interval_minutes=unreplied_notify_interval_minutes,
unreplied_notify_max_count=unreplied_notify_max_count,
)
mission = Mission.objects.create(
merchant=merchant,
@@ -115,6 +160,9 @@ def create_mission(
category=category or _get_default_mission_category(merchant=merchant),
content_type=content_type,
content_id=content_id,
notify_if_unreplied=notify_if_unreplied,
unreplied_notify_interval_minutes=unreplied_notify_interval_minutes,
unreplied_notify_max_count=unreplied_notify_max_count,
)
if participant_ids is not None:
set_mission_participants(mission=mission, participant_ids=participant_ids)
@@ -138,6 +186,9 @@ def update_mission(
content_id: int | None = None,
update_content_object: bool = False,
participant_ids: list[int] | None = None,
notify_if_unreplied=UNSET,
unreplied_notify_interval_minutes=UNSET,
unreplied_notify_max_count=UNSET,
) -> Mission:
mission = Mission.objects.select_for_update().get(pk=mission.pk)
_assert_employee_belongs_to_mission(updated_by, mission, "更新任务的员工")
@@ -160,8 +211,55 @@ def update_mission(
mission.content_id = content_id
update_fields.extend(["content_type", "content_id"])
notify_changed = notify_if_unreplied is not UNSET
interval_changed = unreplied_notify_interval_minutes is not UNSET
max_count_changed = unreplied_notify_max_count is not UNSET
if notify_changed or interval_changed or max_count_changed:
final_notify_if_unreplied = (
notify_if_unreplied
if notify_changed
else mission.notify_if_unreplied
)
final_interval = (
unreplied_notify_interval_minutes
if interval_changed
else mission.unreplied_notify_interval_minutes
)
final_max_count = (
unreplied_notify_max_count
if max_count_changed
else mission.unreplied_notify_max_count
)
_validate_unreplied_notification_config(
notify_if_unreplied=final_notify_if_unreplied,
unreplied_notify_interval_minutes=final_interval,
unreplied_notify_max_count=final_max_count,
)
if notify_changed:
mission.notify_if_unreplied = final_notify_if_unreplied
update_fields.append("notify_if_unreplied")
if interval_changed:
mission.unreplied_notify_interval_minutes = final_interval
update_fields.append("unreplied_notify_interval_minutes")
if max_count_changed:
mission.unreplied_notify_max_count = final_max_count
update_fields.append("unreplied_notify_max_count")
if not final_notify_if_unreplied:
_reset_unreplied_notification_state(mission)
update_fields.extend([
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
])
elif notify_changed or interval_changed:
_reset_unreplied_notification_state(mission)
update_fields.extend([
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
])
if update_fields:
mission.save(update_fields=[*update_fields, "updated_at"])
mission.save(update_fields=[*dict.fromkeys(update_fields), "updated_at"])
if participant_ids is not None:
set_mission_participants(mission=mission, participant_ids=participant_ids)
return mission
@@ -189,7 +287,15 @@ def create_mission_reply(
)
if ends_task and not mission.is_completed:
mission.is_completed = True
mission.save(update_fields=["is_completed", "updated_at"])
_reset_unreplied_notification_state(mission)
mission.save(
update_fields=[
"is_completed",
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
"updated_at",
]
)
_send_signal_on_commit(
mission_completed,
sender=Mission,
@@ -197,6 +303,8 @@ def create_mission_reply(
completed_by=responder,
reply=reply,
)
else:
_clear_unreplied_notification_state_if_active(mission=mission)
_send_signal_on_commit(
mission_replied,
sender=MissionReply,
@@ -223,7 +331,18 @@ def reopen_mission(*, mission: Mission, reopened_by) -> Mission:
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"])
if mission.notify_if_unreplied and not _has_effective_reply(mission=mission):
_reset_unreplied_notification_state(mission)
mission.save(
update_fields=[
"is_completed",
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
"updated_at",
]
)
else:
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,
@@ -263,9 +382,23 @@ def reject_reply(*, reply: MissionReply, rejected_by) -> MissionReply:
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()
has_effective_reply = mission.replies.exclude(pk=reply.pk).filter(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"])
if mission.notify_if_unreplied and not has_effective_reply:
_reset_unreplied_notification_state(mission)
mission.save(
update_fields=[
"is_completed",
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
"updated_at",
]
)
else:
mission.save(update_fields=["is_completed", "updated_at"])
elif mission.notify_if_unreplied and not has_effective_reply:
_clear_unreplied_notification_state_if_active(mission=mission)
_send_signal_on_commit(
mission_reply_rejected,
sender=MissionReply,
@@ -288,7 +421,17 @@ def cancel_mission(*, mission: Mission, cancelled_by) -> Mission:
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"])
_reset_unreplied_notification_state(mission)
mission.save(
update_fields=[
"is_cancelled",
"cancelled_by",
"cancelled_at",
"unreplied_notify_sent_count",
"unreplied_last_notified_at",
"updated_at",
]
)
_send_signal_on_commit(
mission_cancelled,
sender=Mission,
@@ -306,3 +449,10 @@ def set_mission_urgent(*, mission: Mission, updated_by, is_urgent: bool) -> Miss
mission.is_urgent = is_urgent
mission.save(update_fields=["is_urgent", "updated_at"])
return mission
def get_mission_unreplied_due_at(*, mission: Mission):
if mission.unreplied_notify_interval_minutes is None:
return None
base_time = mission.unreplied_last_notified_at or mission.created_at
return base_time + timedelta(minutes=mission.unreplied_notify_interval_minutes)