From 30c7caff414525987fe32c86fbad6ab1d4eb9143 Mon Sep 17 00:00:00 2001 From: pmc <740076875@qq.com> Date: Thu, 7 May 2026 17:34:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(01-01):=20aiapp=20=E6=96=B0=E5=A2=9E=20Cre?= =?UTF-8?q?dentialSlot=20=E5=8D=95=E4=BE=8B=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 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 模型层 --- qy_lty/aiapp/models.py | 43 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/qy_lty/aiapp/models.py b/qy_lty/aiapp/models.py index 4645e48..81f3ed6 100644 --- a/qy_lty/aiapp/models.py +++ b/qy_lty/aiapp/models.py @@ -49,4 +49,45 @@ class ChatMessage(models.Model): verbose_name_plural = '聊天消息' def __str__(self): - return f"{self.sender}: {self.message[:50]} ({self.timestamp})" \ No newline at end of file + 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 \ No newline at end of file