feat: add feishu notification data models
This commit is contained in:
@@ -754,3 +754,202 @@ class ApplicationFormFillNotificationRecord(models.Model):
|
||||
models.Index(fields=["recipient", "send_status"], name="idx_ra_aff_notify_recipient"),
|
||||
models.Index(fields=["send_status", "retry_count"], name="idx_ra_aff_notify_status"),
|
||||
]
|
||||
|
||||
|
||||
class FeishuUserMapping(models.Model):
|
||||
"""Maps a system user to Feishu identifiers maintained by Admin."""
|
||||
|
||||
system_user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="feishu_mapping",
|
||||
)
|
||||
feishu_display_name = models.CharField(max_length=120, blank=True, default="")
|
||||
feishu_open_id = models.CharField(max_length=120, blank=True, default="")
|
||||
feishu_user_id = models.CharField(max_length=120, blank=True, default="")
|
||||
feishu_mobile = models.CharField(max_length=40, blank=True, default="")
|
||||
is_active = models.BooleanField(default=True)
|
||||
remark = models.CharField(max_length=255, blank=True, default="")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "ra_feishu_user_mapping"
|
||||
ordering = ["system_user__username", "id"]
|
||||
indexes = [
|
||||
models.Index(fields=["is_active"], name="idx_ra_feishu_map_active"),
|
||||
models.Index(fields=["feishu_open_id"], name="idx_ra_feishu_map_open"),
|
||||
models.Index(fields=["feishu_user_id"], name="idx_ra_feishu_map_userid"),
|
||||
models.Index(fields=["feishu_mobile"], name="idx_ra_feishu_map_mobile"),
|
||||
]
|
||||
|
||||
def preferred_identifier(self) -> tuple[str, str]:
|
||||
if self.feishu_open_id:
|
||||
return "open_id", self.feishu_open_id
|
||||
if self.feishu_user_id:
|
||||
return "user_id", self.feishu_user_id
|
||||
if self.feishu_mobile:
|
||||
return "mobile", self.feishu_mobile
|
||||
return "missing", ""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.feishu_display_name or self.system_user.get_username()
|
||||
|
||||
|
||||
class FeishuAccessTokenCache(models.Model):
|
||||
"""Caches Feishu tenant_access_token until its expiry time."""
|
||||
|
||||
app_id_hash = models.CharField(max_length=128, unique=True)
|
||||
tenant_access_token = models.TextField(blank=True, default="")
|
||||
expires_at = models.DateTimeField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True, default="")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "ra_feishu_access_token_cache"
|
||||
ordering = ["-updated_at", "-id"]
|
||||
indexes = [
|
||||
models.Index(fields=["app_id_hash"], name="idx_ra_feishu_token_app"),
|
||||
models.Index(fields=["expires_at"], name="idx_ra_feishu_token_exp"),
|
||||
]
|
||||
|
||||
def is_valid(self, now=None) -> bool:
|
||||
from django.utils import timezone
|
||||
|
||||
current = now or timezone.now()
|
||||
return bool(self.tenant_access_token and self.expires_at and self.expires_at > current)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Feishu token cache {self.app_id_hash[:8]}"
|
||||
|
||||
|
||||
class WorkflowNotificationRecord(models.Model):
|
||||
"""Stores unified notification send records for all workflow types."""
|
||||
|
||||
class Channel(models.TextChoices):
|
||||
MOCK = "mock", "模拟"
|
||||
DISABLED = "disabled", "未启用"
|
||||
FEISHU_API = "feishu_api", "飞书 API"
|
||||
|
||||
class SendStatus(models.TextChoices):
|
||||
PENDING = "pending", "待发送"
|
||||
SUCCESS = "success", "成功"
|
||||
FAILED = "failed", "失败"
|
||||
SKIPPED_DUPLICATE = "skipped_duplicate", "重复跳过"
|
||||
DISABLED = "disabled", "未启用"
|
||||
|
||||
workflow_type = models.CharField(max_length=40)
|
||||
workflow_batch_id = models.PositiveBigIntegerField()
|
||||
workflow_batch_no = models.CharField(max_length=80)
|
||||
workflow_status = models.CharField(max_length=40)
|
||||
dedupe_key = models.CharField(max_length=160)
|
||||
trigger_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workflow_notification_records",
|
||||
)
|
||||
feishu_mapping = models.ForeignKey(
|
||||
FeishuUserMapping,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="notification_records",
|
||||
)
|
||||
channel = models.CharField(max_length=40, choices=Channel.choices, default=Channel.MOCK)
|
||||
target = models.CharField(max_length=160, blank=True, default="")
|
||||
at_display_name = models.CharField(max_length=120, blank=True, default="")
|
||||
at_identifier_type = models.CharField(max_length=30, blank=True, default="")
|
||||
at_identifier_masked = models.CharField(max_length=120, blank=True, default="")
|
||||
send_status = models.CharField(
|
||||
max_length=30,
|
||||
choices=SendStatus.choices,
|
||||
default=SendStatus.PENDING,
|
||||
)
|
||||
message_title = models.CharField(max_length=200)
|
||||
message_summary = models.TextField(blank=True, default="")
|
||||
result_url = models.CharField(max_length=500, blank=True, default="")
|
||||
external_message_id = models.CharField(max_length=120, blank=True, default="")
|
||||
error_code = models.CharField(max_length=80, blank=True, default="")
|
||||
error_message = models.TextField(blank=True, default="")
|
||||
request_duration_ms = models.PositiveIntegerField(null=True, blank=True)
|
||||
sent_at = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "ra_workflow_notification_record"
|
||||
ordering = ["-created_at", "-id"]
|
||||
indexes = [
|
||||
models.Index(fields=["workflow_type", "workflow_batch_id"], name="idx_ra_notify_workflow"),
|
||||
models.Index(fields=["trigger_user", "created_at"], name="idx_ra_notify_user_created"),
|
||||
models.Index(fields=["send_status", "created_at"], name="idx_ra_notify_status"),
|
||||
models.Index(fields=["workflow_batch_no"], name="idx_ra_notify_batch_no"),
|
||||
models.Index(fields=["dedupe_key", "send_status"], name="idx_ra_notify_dedupe_status"),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def build_dedupe_key(cls, workflow_type: str, workflow_batch_id: int, workflow_status: str) -> str:
|
||||
return f"{workflow_type}:{workflow_batch_id}:{workflow_status}"
|
||||
|
||||
@classmethod
|
||||
def already_successfully_sent(cls, dedupe_key: str) -> bool:
|
||||
return cls.objects.filter(dedupe_key=dedupe_key, send_status=cls.SendStatus.SUCCESS).exists()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.workflow_type} {self.workflow_batch_no} {self.send_status}"
|
||||
|
||||
|
||||
class FeishuQuestionLog(models.Model):
|
||||
"""Records reserved Feishu question handling without storing full answers."""
|
||||
|
||||
class SourceType(models.TextChoices):
|
||||
PRIVATE_CHAT = "private_chat", "私聊"
|
||||
GROUP_MENTION = "group_mention", "群聊 @"
|
||||
SIMULATE = "simulate", "本地模拟"
|
||||
|
||||
class Status(models.TextChoices):
|
||||
SUCCESS = "success", "成功"
|
||||
FAILED = "failed", "失败"
|
||||
IGNORED = "ignored", "忽略"
|
||||
|
||||
system_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="feishu_question_logs",
|
||||
)
|
||||
feishu_mapping = models.ForeignKey(
|
||||
FeishuUserMapping,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="question_logs",
|
||||
)
|
||||
feishu_open_id = models.CharField(max_length=120, blank=True, default="")
|
||||
feishu_user_id = models.CharField(max_length=120, blank=True, default="")
|
||||
source_type = models.CharField(max_length=30, choices=SourceType.choices, default=SourceType.SIMULATE)
|
||||
message_id = models.CharField(max_length=120, blank=True, default="")
|
||||
question_text = models.TextField()
|
||||
intent = models.CharField(max_length=60, blank=True, default="")
|
||||
query_object = models.JSONField(default=dict, blank=True)
|
||||
answer_summary = models.TextField(blank=True, default="")
|
||||
permission_result = models.CharField(max_length=40, blank=True, default="")
|
||||
status = models.CharField(max_length=30, choices=Status.choices, default=Status.SUCCESS)
|
||||
error_message = models.TextField(blank=True, default="")
|
||||
processed_at = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "ra_feishu_question_log"
|
||||
ordering = ["-created_at", "-id"]
|
||||
indexes = [
|
||||
models.Index(fields=["system_user", "created_at"], name="idx_ra_feishu_q_user_created"),
|
||||
models.Index(fields=["intent", "created_at"], name="idx_ra_feishu_q_intent"),
|
||||
models.Index(fields=["status", "created_at"], name="idx_ra_feishu_q_status"),
|
||||
models.Index(fields=["message_id"], name="idx_ra_feishu_q_message"),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.intent or 'unknown'} {self.status}"
|
||||
|
||||
Reference in New Issue
Block a user