- 在 aiapp/models.py 末尾追加 CredentialSlot(不动 Bot / ChatMessage) - 字段:app_id CharField(128) / access_token CharField(512) / updated_at auto_now - 单例三件套:pk=1 + save() 钩子重定向 + get_solo() 类方法(1:1 复刻 AffinitySetting) - 不引入 gettext_lazy / created_at,沿用仓库中文 verbose_name 实操约定 - 覆盖需求 CRED-01 模型层
93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
from django.db import models
|
||
from userapp.models import ParadiseUser
|
||
|
||
|
||
class Bot(models.Model):
|
||
name = models.CharField(max_length=255, verbose_name='机器人姓名')
|
||
description = models.TextField(blank=False, null=False, verbose_name='机器人提示词')
|
||
|
||
class Meta:
|
||
verbose_name = '机器人'
|
||
verbose_name_plural = '机器人'
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class ChatMessage(models.Model):
|
||
SENDER_USER = 'user'
|
||
SENDER_BOT = 'assistant'
|
||
SENDER_SYSTEM = 'system'
|
||
|
||
SENDER_CHOICES = [
|
||
(SENDER_USER, 'user'),
|
||
(SENDER_BOT, 'assistant'),
|
||
(SENDER_SYSTEM, 'system'),
|
||
]
|
||
|
||
MESSAGE_TYPE_TEXT = 'text'
|
||
MESSAGE_TYPE_AUDIO = 'audio'
|
||
MESSAGE_TYPE_VIDEO = 'video'
|
||
|
||
MESSAGE_TYPE_CHOICES = [
|
||
(MESSAGE_TYPE_TEXT, 'text'),
|
||
(MESSAGE_TYPE_AUDIO, 'audio'),
|
||
(MESSAGE_TYPE_VIDEO, 'video'),
|
||
]
|
||
|
||
user = models.ForeignKey(ParadiseUser, on_delete=models.CASCADE, verbose_name='用户')
|
||
bot = models.ForeignKey(Bot, on_delete=models.CASCADE, verbose_name='机器人')
|
||
message = models.TextField(max_length=2048, null=False, blank=False, verbose_name='消息内容')
|
||
message_audio_url = models.TextField(max_length=2048, blank=True, verbose_name='消息内容语音链接')
|
||
message_video_url = models.TextField(max_length=2048, blank=True, verbose_name='消息内容视频链接')
|
||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name='时间戳')
|
||
sender = models.CharField(max_length=10, choices=SENDER_CHOICES, verbose_name='发送者')
|
||
message_type = models.CharField(max_length=10, choices=MESSAGE_TYPE_CHOICES, default=MESSAGE_TYPE_TEXT, verbose_name='消息类型')
|
||
|
||
class Meta:
|
||
ordering = ['timestamp'] # 按时间顺序排序
|
||
verbose_name = '聊天消息'
|
||
verbose_name_plural = '聊天消息'
|
||
|
||
def __str__(self):
|
||
return f"{self.sender}: {self.message[:50]} ({self.timestamp})"
|
||
|
||
|
||
class CredentialSlot(models.Model):
|
||
"""通用凭据槽位(单例)— Milestone v1.0 / Phase 1
|
||
|
||
全局唯一一条记录,存第三方服务(Kimi / 阿里云 / 火山等)的 APP ID + Access Token。
|
||
通过 pk=1 + get_or_create + save() 钩子三件套保证单例:
|
||
- 任何 .save() 在已有记录时把新对象 pk 改成现有那条
|
||
- get_solo() 是单一访问入口(Phase 2/3 视图统一调用)
|
||
- DB 层无额外约束,绕过 ORM 的 bulk_create / 原始 SQL 不在保护范围
|
||
"""
|
||
|
||
app_id = models.CharField(
|
||
'APP ID', max_length=128, blank=True, default='',
|
||
help_text='第三方服务商分配的 APP ID;运营在 Admin 录入'
|
||
)
|
||
access_token = models.CharField(
|
||
'Access Token', max_length=512, blank=True, default='',
|
||
help_text='第三方服务商访问令牌;DB 明文存储,Admin 列表/查看态末 4 位脱敏'
|
||
)
|
||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||
|
||
class Meta:
|
||
verbose_name = '凭据槽位'
|
||
verbose_name_plural = '凭据槽位'
|
||
|
||
def __str__(self):
|
||
return f"凭据槽位 (updated {self.updated_at:%Y-%m-%d %H:%M})"
|
||
|
||
def save(self, *args, **kwargs):
|
||
# 强制单例:新增时如果已有记录则覆盖到现有 pk
|
||
if not self.pk and CredentialSlot.objects.exists():
|
||
existing = CredentialSlot.objects.first()
|
||
self.pk = existing.pk
|
||
super().save(*args, **kwargs)
|
||
|
||
@classmethod
|
||
def get_solo(cls):
|
||
"""获取单例实例,不存在则用默认值创建(pk=1)"""
|
||
instance, _ = cls.objects.get_or_create(pk=1)
|
||
return instance |