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