Files

1180 lines
46 KiB
Python

from django.conf import settings
from django.db import models
class Conversation(models.Model):
"""Stores a user's review-agent conversation shell."""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_conversations",
)
title = models.CharField(max_length=120, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-updated_at", "-id"]
def __str__(self) -> str:
return self.title or f"对话 {self.pk}"
class Message(models.Model):
"""Stores one user or assistant message in a conversation."""
class Role(models.TextChoices):
USER = "user", "用户"
ASSISTANT = "assistant", "助手"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="messages",
)
role = models.CharField(max_length=20, choices=Role.choices)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at", "id"]
def __str__(self) -> str:
return f"{self.get_role_display()} - {self.conversation_id}"
class FileAttachment(models.Model):
"""Stores an uploaded file version for one conversation."""
class UploadStatus(models.TextChoices):
UPLOADED = "uploaded", "已上传"
BOUND = "bound", "已绑定"
DELETED = "deleted", "已删除"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="file_attachments",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_file_attachments",
)
original_name = models.CharField(max_length=255)
version_no = models.PositiveIntegerField(default=1)
is_active = models.BooleanField(default=True)
storage_path = models.CharField(max_length=500)
file_size = models.BigIntegerField(default=0)
content_type = models.CharField(max_length=120, blank=True, default="")
upload_status = models.CharField(
max_length=20,
choices=UploadStatus.choices,
default=UploadStatus.UPLOADED,
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "ra_file_attachment"
ordering = ["-created_at", "-id"]
constraints = [
models.UniqueConstraint(
fields=["conversation", "original_name", "version_no"],
name="uq_ra_attachment_conv_name_version",
)
]
indexes = [
models.Index(
fields=["conversation", "created_at"],
name="idx_ra_attachment_conv_created",
),
models.Index(
fields=["user", "created_at"],
name="idx_ra_attachment_user_created",
),
models.Index(
fields=["conversation", "original_name", "is_active"],
name="idx_ra_attachment_active",
),
]
def __str__(self) -> str:
return f"{self.original_name} v{self.version_no}"
class FileSummaryBatch(models.Model):
"""Tracks one automatic file inventory and page-count workflow run."""
class Status(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="file_summary_batches",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_file_summary_batches",
)
trigger_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="triggered_file_summary_batches",
)
batch_no = models.CharField(max_length=64, unique=True)
product_name = models.CharField(max_length=200, blank=True, default="")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
total_files = models.IntegerField(default=0)
supported_files = models.IntegerField(default=0)
success_files = models.IntegerField(default=0)
failed_files = models.IntegerField(default=0)
unsupported_files = models.IntegerField(default=0)
uncertain_files = models.IntegerField(default=0)
total_pages = models.IntegerField(default=0)
work_dir = models.CharField(max_length=500, blank=True, default="")
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
finished_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "ra_file_summary_batch"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["conversation", "created_at"], name="idx_ra_batch_conv_created"),
models.Index(fields=["user", "created_at"], name="idx_ra_batch_user_created"),
models.Index(fields=["status", "created_at"], name="idx_ra_batch_status"),
]
def __str__(self) -> str:
return self.batch_no
class FileSummaryBatchAttachment(models.Model):
"""Binds a workflow batch to the exact attachment versions it uses."""
class SourceRole(models.TextChoices):
ARCHIVE = "archive", "压缩包"
MULTI_FILE = "multi_file", "多文件"
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
related_name="batch_attachments",
)
attachment = models.ForeignKey(
FileAttachment,
on_delete=models.CASCADE,
related_name="batch_bindings",
)
source_role = models.CharField(
max_length=20,
choices=SourceRole.choices,
default=SourceRole.MULTI_FILE,
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "ra_file_summary_batch_attachment"
constraints = [
models.UniqueConstraint(
fields=["batch", "attachment"],
name="uq_ra_batch_attachment",
)
]
indexes = [
models.Index(
fields=["batch", "created_at"],
name="idx_ra_batch_attachment_batch",
),
models.Index(fields=["attachment"], name="idx_ra_batch_attach_file"),
]
class FileSummaryItem(models.Model):
"""Stores one scanned file and its page-count result."""
class StatisticsStatus(models.TextChoices):
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
UNSUPPORTED = "unsupported", "不支持"
UNCERTAIN = "uncertain", "不确定"
SKIPPED = "skipped", "跳过"
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
related_name="items",
)
file_index = models.PositiveIntegerField()
directory_level = models.CharField(max_length=300, blank=True, default="")
file_name = models.CharField(max_length=255)
file_type = models.CharField(max_length=20)
relative_path = models.CharField(max_length=500)
storage_path = models.CharField(max_length=500)
page_count = models.IntegerField(null=True, blank=True)
statistics_status = models.CharField(
max_length=20,
choices=StatisticsStatus.choices,
default=StatisticsStatus.SKIPPED,
)
retry_count = models.PositiveIntegerField(default=0)
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_file_summary_item"
ordering = ["file_index", "id"]
constraints = [
models.UniqueConstraint(
fields=["batch", "relative_path"],
name="uq_ra_item_batch_relative_path",
)
]
indexes = [
models.Index(fields=["batch", "file_index"], name="idx_ra_item_batch_index"),
models.Index(fields=["batch", "statistics_status"], name="idx_ra_item_batch_status"),
models.Index(fields=["batch", "file_type"], name="idx_ra_item_batch_type"),
]
class WorkflowNodeRun(models.Model):
"""Stores recoverable status for one workflow node."""
class Status(models.TextChoices):
PENDING = "pending", "等待中"
RUNNING = "running", "执行中"
WAITING_USER = "waiting_user", "等待用户确认"
RETRYING = "retrying", "重试中"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
SKIPPED = "skipped", "跳过"
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="node_runs",
)
workflow_type = models.CharField(max_length=40, blank=True, default="file_summary")
workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True)
node_group = models.CharField(max_length=40, blank=True, default="file_summary")
node_code = models.CharField(max_length=40)
node_name = models.CharField(max_length=80)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
progress = models.PositiveIntegerField(default=0)
message = models.TextField(blank=True, default="")
started_at = models.DateTimeField(null=True, blank=True)
finished_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "ra_workflow_node_run"
constraints = [
models.UniqueConstraint(fields=["batch", "node_code"], name="uq_ra_node_batch_code"),
models.UniqueConstraint(
fields=["workflow_type", "workflow_batch_id", "node_code"],
name="uq_ra_node_workflow_batch_code",
),
]
indexes = [
models.Index(fields=["batch", "status"], name="idx_ra_node_batch_status"),
models.Index(
fields=["workflow_type", "workflow_batch_id"],
name="idx_ra_node_workflow",
),
]
class WorkflowEvent(models.Model):
"""Persists workflow events for SSE replay and diagnostics."""
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="events",
)
workflow_type = models.CharField(max_length=40, blank=True, default="file_summary")
workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True)
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="workflow_events",
)
event_type = models.CharField(max_length=40)
payload = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "ra_workflow_event"
ordering = ["id"]
indexes = [
models.Index(fields=["batch", "id"], name="idx_ra_event_batch_id"),
models.Index(fields=["batch", "created_at"], name="idx_ra_event_batch_created"),
models.Index(
fields=["workflow_type", "workflow_batch_id", "id"],
name="idx_ra_event_workflow_id",
),
]
class ExportedSummaryFile(models.Model):
"""Stores generated report files for permission-checked download."""
class ExportType(models.TextChoices):
MARKDOWN = "markdown", "Markdown"
EXCEL = "excel", "Excel"
JSON = "json", "JSON"
WORD = "word", "Word"
PDF = "pdf", "PDF"
ZIP = "zip", "ZIP"
class Status(models.TextChoices):
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
related_name="exports",
null=True,
blank=True,
)
workflow_type = models.CharField(max_length=40, blank=True, default="file_summary")
workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True)
export_category = models.CharField(max_length=40, blank=True, default="summary")
export_type = models.CharField(max_length=20, choices=ExportType.choices)
file_name = models.CharField(max_length=255)
storage_path = models.CharField(max_length=500)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.SUCCESS)
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "ra_exported_summary_file"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "export_type"], name="idx_ra_export_batch_type"),
models.Index(fields=["batch", "created_at"], name="idx_ra_export_batch_created"),
models.Index(
fields=["workflow_type", "workflow_batch_id"],
name="idx_ra_export_workflow",
),
]
class RegulatoryRuleVersion(models.Model):
"""Tracks the local regulatory rule YAML and its matching RAG index."""
class Status(models.TextChoices):
ACTIVE = "active", "启用"
OUTDATED = "outdated", "待更新"
DISABLED = "disabled", "停用"
code = models.CharField(max_length=80, unique=True)
name = models.CharField(max_length=160)
yaml_path = models.CharField(max_length=500)
yaml_hash = models.CharField(max_length=128)
rag_collection = models.CharField(max_length=120, blank=True, default="")
rag_index_version = models.CharField(max_length=80, blank=True, default="")
rag_index_hash = models.CharField(max_length=128, blank=True, default="")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "ra_regulatory_rule_version"
ordering = ["-updated_at", "-id"]
indexes = [
models.Index(fields=["code", "status"], name="idx_ra_rule_code_status"),
]
def __str__(self) -> str:
return self.code
class KnowledgeBaseDocument(models.Model):
"""Stores user-managed knowledge-base source documents."""
class Status(models.TextChoices):
ACTIVE = "active", "启用"
DISABLED = "disabled", "停用"
DELETED = "deleted", "已删除"
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="knowledge_base_documents",
)
display_name = models.CharField(max_length=255)
original_name = models.CharField(max_length=255)
storage_path = models.CharField(max_length=500)
file_size = models.BigIntegerField(default=0)
content_type = models.CharField(max_length=120, blank=True, default="")
description = models.TextField(blank=True, default="")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE)
is_active = models.BooleanField(default=True)
indexed_chunk_count = models.PositiveIntegerField(default=0)
metadata = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "ra_knowledge_base_document"
ordering = ["-updated_at", "-id"]
indexes = [
models.Index(fields=["user", "status"], name="idx_ra_kb_doc_user_status"),
models.Index(fields=["user", "created_at"], name="idx_ra_kb_doc_user_created"),
models.Index(fields=["status", "updated_at"], name="idx_ra_kb_doc_status_updated"),
]
def __str__(self) -> str:
return self.display_name
class ApplicationFormFillBatch(models.Model):
"""Tracks one application-form auto-fill workflow run."""
class Status(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
WAITING_USER = "waiting_user", "等待用户"
SUCCESS = "success", "成功"
PARTIAL_SUCCESS = "partial_success", "部分成功"
FAILED = "failed", "失败"
CANCELLED = "cancelled", "已取消"
class RegistrationTypeSource(models.TextChoices):
USER_MESSAGE = "user_message", "用户话语"
REGULATORY_BATCH = "regulatory_batch", "法规核查批次"
FILE_EXTRACT = "file_extract", "文件抽取"
UNKNOWN = "unknown", "未知"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="application_form_fill_batches",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_application_form_fill_batches",
)
trigger_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="triggered_application_form_fill_batches",
)
source_summary_batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.PROTECT,
related_name="application_form_fill_batches",
)
source_regulatory_batch = models.ForeignKey(
"RegulatoryReviewBatch",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="application_form_fill_batches",
)
batch_no = models.CharField(max_length=64, unique=True)
status = models.CharField(max_length=30, choices=Status.choices, default=Status.PENDING)
requested_templates = models.JSONField(default=list, blank=True)
selected_templates = models.JSONField(default=list, blank=True)
output_types = models.JSONField(default=list, blank=True)
registration_type = models.CharField(max_length=80, blank=True, default="")
registration_type_source = models.CharField(
max_length=40,
choices=RegistrationTypeSource.choices,
default=RegistrationTypeSource.UNKNOWN,
)
product_name = models.CharField(max_length=200, blank=True, default="")
conflict_summary = models.JSONField(default=list, blank=True)
risk_notes = models.JSONField(default=list, blank=True)
template_config_version = models.CharField(max_length=80, blank=True, default="")
template_config_hash = models.CharField(max_length=128, blank=True, default="")
work_dir = models.CharField(max_length=500, blank=True, default="")
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
finished_at = models.DateTimeField(null=True, blank=True)
archived_at = models.DateTimeField(null=True, blank=True)
is_deleted = models.BooleanField(default=False)
class Meta:
db_table = "ra_application_form_fill_batch"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["conversation", "status"], name="idx_ra_aff_batch_conv_status"),
models.Index(fields=["source_summary_batch"], name="idx_ra_aff_batch_summary"),
models.Index(fields=["source_regulatory_batch"], name="idx_ra_aff_batch_regulatory"),
models.Index(fields=["user", "created_at"], name="idx_ra_aff_batch_user_created"),
models.Index(fields=["created_at"], name="idx_ra_aff_batch_created"),
]
def __str__(self) -> str:
return self.batch_no
class RegulatoryInfoPackageBatch(models.Model):
"""Tracks one Chapter 1 regulatory information package workflow run."""
class Status(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
WAITING_USER = "waiting_user", "等待用户"
SUCCESS = "success", "成功"
PARTIAL_SUCCESS = "partial_success", "部分成功"
FAILED = "failed", "失败"
CANCELLED = "cancelled", "已取消"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="regulatory_info_package_batches",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_regulatory_info_package_batches",
)
trigger_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="triggered_regulatory_info_package_batches",
)
source_attachment = models.ForeignKey(
FileAttachment,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="regulatory_info_package_batches",
)
source_summary_batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="regulatory_info_package_batches",
)
source_summary_item_id = models.PositiveBigIntegerField(null=True, blank=True)
batch_no = models.CharField(max_length=64, unique=True)
status = models.CharField(max_length=30, choices=Status.choices, default=Status.PENDING)
source_file_name = models.CharField(max_length=255, blank=True, default="")
source_storage_path = models.CharField(max_length=500, blank=True, default="")
product_name = models.CharField(max_length=200, blank=True, default="")
output_zip_name = models.CharField(max_length=255, blank=True, default="第1章 监管信息(预生成版).zip")
generated_files = models.JSONField(default=list, blank=True)
missing_fields = models.JSONField(default=list, blank=True)
llm_only_fields = models.JSONField(default=list, blank=True)
conflict_fields = models.JSONField(default=list, blank=True)
risk_notes = models.JSONField(default=list, blank=True)
template_config_version = models.CharField(max_length=80, blank=True, default="")
template_config_hash = models.CharField(max_length=128, blank=True, default="")
adapter_summary = models.JSONField(default=dict, blank=True)
work_dir = models.CharField(max_length=500, blank=True, default="")
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
finished_at = models.DateTimeField(null=True, blank=True)
archived_at = models.DateTimeField(null=True, blank=True)
is_deleted = models.BooleanField(default=False)
class Meta:
db_table = "ra_regulatory_info_package_batch"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["conversation", "status"], name="idx_ra_rip_batch_conv_status"),
models.Index(fields=["user", "created_at"], name="idx_ra_rip_batch_user_created"),
models.Index(fields=["source_attachment"], name="idx_ra_rip_batch_attachment"),
models.Index(fields=["source_summary_batch"], name="idx_ra_rip_batch_summary"),
models.Index(fields=["created_at"], name="idx_ra_rip_batch_created"),
]
def __str__(self) -> str:
return self.batch_no
class RegulatoryReviewBatch(models.Model):
"""Tracks one NMPA regulatory review workflow run."""
class Status(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
WAITING_USER = "waiting_user", "等待用户确认"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="regulatory_review_batches",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_regulatory_batches",
)
trigger_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="triggered_regulatory_batches",
)
source_summary_batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.PROTECT,
related_name="regulatory_review_batches",
)
rule_version = models.ForeignKey(
RegulatoryRuleVersion,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="review_batches",
)
batch_no = models.CharField(max_length=64, unique=True)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
condition_json = models.JSONField(default=dict, blank=True)
risk_summary = models.JSONField(default=dict, blank=True)
work_dir = models.CharField(max_length=500, blank=True, default="")
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
finished_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "ra_regulatory_review_batch"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["conversation", "created_at"], name="idx_ra_rr_batch_conv"),
models.Index(fields=["user", "created_at"], name="idx_ra_rr_batch_user"),
models.Index(fields=["status", "created_at"], name="idx_ra_rr_batch_status"),
]
def __str__(self) -> str:
return self.batch_no
class RegulatoryIssue(models.Model):
"""Stores one regulatory finding after risk consolidation."""
class Severity(models.TextChoices):
BLOCKING = "blocking", "阻断项"
HIGH = "high", "高风险"
MEDIUM = "medium", "中风险"
LOW = "low", "低风险"
INFO = "info", "提示"
class Category(models.TextChoices):
COMPLETENESS = "completeness", "完整性"
STRUCTURE = "structure", "章节"
CONSISTENCY = "consistency", "一致性"
RAG = "rag", "法规依据"
class Status(models.TextChoices):
OPEN = "open", "待处理"
RESOLVED = "resolved", "已整改"
ACCEPTED = "accepted", "已接受"
REVIEW_PASSED = "review_passed", "复核通过"
REVIEW_FAILED = "review_failed", "复核未通过"
batch = models.ForeignKey(
RegulatoryReviewBatch,
on_delete=models.CASCADE,
related_name="issues",
)
rule_code = models.CharField(max_length=120, blank=True, default="")
category = models.CharField(max_length=40, choices=Category.choices)
severity = models.CharField(max_length=20, choices=Severity.choices)
title = models.CharField(max_length=255)
detail = models.TextField(blank=True, default="")
suggestion = models.TextField(blank=True, default="")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN)
evidence = models.JSONField(default=dict, blank=True)
citations = models.JSONField(default=list, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "ra_regulatory_issue"
ordering = ["severity", "id"]
indexes = [
models.Index(fields=["batch", "severity"], name="idx_ra_rr_issue_severity"),
models.Index(fields=["batch", "category"], name="idx_ra_rr_issue_category"),
]
def __str__(self) -> str:
return self.title
class RegulatoryArtifact(models.Model):
"""Stores regulatory review intermediate and exported artifacts."""
class ArtifactType(models.TextChoices):
MARKDOWN = "markdown", "Markdown"
EXCEL = "excel", "Excel"
JSON = "json", "JSON"
TEXT = "text", "文本"
batch = models.ForeignKey(
RegulatoryReviewBatch,
on_delete=models.CASCADE,
related_name="artifacts",
)
artifact_type = models.CharField(max_length=20, choices=ArtifactType.choices)
name = models.CharField(max_length=160)
storage_path = models.CharField(max_length=500)
content_hash = models.CharField(max_length=128, blank=True, default="")
metadata = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "ra_regulatory_artifact"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "artifact_type"], name="idx_ra_rr_artifact_type"),
]
class RegulatoryNotificationRecord(models.Model):
"""Stores mock notification records for future Feishu integration."""
class Channel(models.TextChoices):
MOCK = "mock", "模拟"
FEISHU = "feishu", "飞书"
class Status(models.TextChoices):
PENDING = "pending", "待发送"
SENT = "sent", "已发送"
FAILED = "failed", "失败"
batch = models.ForeignKey(
RegulatoryReviewBatch,
on_delete=models.CASCADE,
related_name="notifications",
)
channel = models.CharField(max_length=20, choices=Channel.choices, default=Channel.MOCK)
target = models.CharField(max_length=160, blank=True, default="")
payload = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
sent_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "ra_regulatory_notification_record"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "status"], name="idx_ra_rr_notify_status"),
]
class ApplicationFormFillArtifact(models.Model):
"""Stores auto-fill intermediate files and generated artifacts."""
class ArtifactType(models.TextChoices):
TEMPLATE_COPY = "template_copy", "模板副本"
FIELD_EXTRACT_RESULT = "field_extract_result", "字段抽取结果"
MERGED_FIELDS = "merged_fields", "字段合并结果"
TRACEABILITY = "traceability", "追溯清单"
FILLED_TEMPLATE = "filled_template", "已填模板"
NOTIFICATION_RECORD = "notification_record", "通知记录"
class FileFormat(models.TextChoices):
JSON = "json", "JSON"
EXCEL = "excel", "Excel"
DOCX = "docx", "DOCX"
PDF = "pdf", "PDF"
MARKDOWN = "markdown", "Markdown"
batch = models.ForeignKey(
ApplicationFormFillBatch,
on_delete=models.CASCADE,
related_name="artifacts",
)
artifact_type = models.CharField(max_length=60, choices=ArtifactType.choices)
file_format = models.CharField(max_length=20, choices=FileFormat.choices)
name = models.CharField(max_length=160)
file_name = models.CharField(max_length=255)
storage_path = models.CharField(max_length=500)
file_size = models.BigIntegerField(default=0)
content_hash = models.CharField(max_length=128, blank=True, default="")
metadata = models.JSONField(default=dict, blank=True)
created_by_node = models.CharField(max_length=60, blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
is_deleted = models.BooleanField(default=False)
class Meta:
db_table = "ra_application_form_fill_artifact"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "artifact_type"], name="idx_ra_aff_artifact_batch_type"),
models.Index(fields=["file_format"], name="idx_ra_aff_artifact_format"),
models.Index(fields=["created_at"], name="idx_ra_aff_artifact_created"),
]
class RegulatoryInfoPackageArtifact(models.Model):
"""Stores regulatory information package intermediate and generated files."""
class ArtifactType(models.TextChoices):
TEMPLATE_COPY = "template_copy", "模板副本"
INSTRUCTION_EXTRACT = "instruction_extract", "说明书抽取结果"
FIELD_EXTRACT_RESULT = "field_extract_result", "字段抽取结果"
MERGED_FIELDS = "merged_fields", "合并字段"
GENERATED_DOCUMENT = "generated_document", "生成文件"
TRACEABILITY = "traceability", "追溯清单"
ZIP_PACKAGE = "zip_package", "ZIP包"
NOTIFICATION_RECORD = "notification_record", "通知记录"
class FileFormat(models.TextChoices):
JSON = "json", "JSON"
EXCEL = "excel", "Excel"
DOCX = "docx", "DOCX"
DOC = "doc", "DOC"
ZIP = "zip", "ZIP"
MARKDOWN = "markdown", "Markdown"
batch = models.ForeignKey(
RegulatoryInfoPackageBatch,
on_delete=models.CASCADE,
related_name="artifacts",
)
artifact_type = models.CharField(max_length=60, choices=ArtifactType.choices)
file_format = models.CharField(max_length=20, choices=FileFormat.choices)
name = models.CharField(max_length=160)
file_name = models.CharField(max_length=255)
storage_path = models.CharField(max_length=500)
file_size = models.BigIntegerField(default=0)
content_hash = models.CharField(max_length=128, blank=True, default="")
metadata = models.JSONField(default=dict, blank=True)
created_by_node = models.CharField(max_length=60, blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
is_deleted = models.BooleanField(default=False)
class Meta:
db_table = "ra_regulatory_info_package_artifact"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "artifact_type"], name="idx_ra_rip_artifact_batch_type"),
models.Index(fields=["file_format"], name="idx_ra_rip_artifact_format"),
models.Index(fields=["created_at"], name="idx_ra_rip_artifact_created"),
]
class ApplicationFormFillNotificationRecord(models.Model):
"""Stores mock/Feishu notification records for application-form auto-fill."""
class Channel(models.TextChoices):
FEISHU_CLI = "feishu_cli", "飞书 CLI"
FEISHU_API = "feishu_api", "飞书 API"
MOCK = "mock", "模拟"
class SendStatus(models.TextChoices):
PENDING = "pending", "待发送"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
batch = models.ForeignKey(
ApplicationFormFillBatch,
on_delete=models.CASCADE,
related_name="notifications",
)
recipient = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="application_form_fill_notifications",
)
channel = models.CharField(max_length=30, choices=Channel.choices, default=Channel.MOCK)
template_codes = models.JSONField(default=list, blank=True)
export_ids = models.JSONField(default=list, blank=True)
message_summary = models.TextField(blank=True, default="")
send_status = models.CharField(
max_length=20,
choices=SendStatus.choices,
default=SendStatus.PENDING,
)
retry_count = models.PositiveIntegerField(default=0)
external_message_id = models.CharField(max_length=120, blank=True, default="")
error_message = models.TextField(blank=True, default="")
sent_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_deleted = models.BooleanField(default=False)
class Meta:
db_table = "ra_application_form_fill_notification_record"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "created_at"], name="idx_ra_aff_notify_batch"),
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 RegulatoryInfoPackageNotificationRecord(models.Model):
"""Stores mock/Feishu notification records for regulatory info packages."""
class Channel(models.TextChoices):
FEISHU_CLI = "feishu_cli", "飞书 CLI"
FEISHU_API = "feishu_api", "飞书 API"
MOCK = "mock", "模拟"
class SendStatus(models.TextChoices):
PENDING = "pending", "待发送"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
batch = models.ForeignKey(
RegulatoryInfoPackageBatch,
on_delete=models.CASCADE,
related_name="notifications",
)
recipient = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="regulatory_info_package_notifications",
)
channel = models.CharField(max_length=30, choices=Channel.choices, default=Channel.MOCK)
export_ids = models.JSONField(default=list, blank=True)
message_summary = models.TextField(blank=True, default="")
send_status = models.CharField(
max_length=20,
choices=SendStatus.choices,
default=SendStatus.PENDING,
)
retry_count = models.PositiveIntegerField(default=0)
external_message_id = models.CharField(max_length=120, blank=True, default="")
error_message = models.TextField(blank=True, default="")
sent_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_deleted = models.BooleanField(default=False)
class Meta:
db_table = "ra_regulatory_info_package_notification_record"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "created_at"], name="idx_ra_rip_notify_batch"),
models.Index(fields=["recipient", "send_status"], name="idx_ra_rip_notify_recipient"),
models.Index(fields=["send_status", "retry_count"], name="idx_ra_rip_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}"