第二批(T6~T11)协调提交: - 迁移 0005:kb 七表(entity/relation/rule/chunk vector1024+tsv 生成列/ asset/color/gap),HNSW+GIN 索引,FK 级联 - ORM:七个 Kb* 模型(pgvector Vector/TSVECTOR/Computed) - 配置:检索与 embedding、华为 JWKS、短信全参数组 - 错误码:40110/40111/42910/42911/61000 - 骨架路由:kb.py + auth_sms.py(已注册);依赖:tencentcloud-sdk-python-sms + spike extra(onnxruntime/opencv);gitignore:models/ 与 spike 输出
482 lines
22 KiB
Python
482 lines
22 KiB
Python
"""SQLAlchemy 2.0 ORM models.
|
||
|
||
Scope of this scaffold: only the tables needed to prove the vertical slice
|
||
(架构文档 §6.2 identity / billing / task / 观测与成本). All remaining domains
|
||
(pf_ / rp_ / kb_ / ag_ / tr_ / cp_) are intentionally absent — see
|
||
packages/*/README.md for what still has to be built.
|
||
|
||
Conventions enforced here (架构文档 §6.1):
|
||
1. 表名带域前缀 (`id_` / `bl_` / `tk_` / `ob_`),为将来拆库预留边界。
|
||
2. 主键统一雪花 ID (bigint),不用自增、不用 UUID。
|
||
3. 涉及配额的表必须有幂等键与流水。
|
||
4. JSONB 只用于「结构会演进且不需要索引全部字段」的场景。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from pgvector.sqlalchemy import Vector
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
Boolean,
|
||
Computed,
|
||
DateTime,
|
||
Index,
|
||
Integer,
|
||
Numeric,
|
||
PrimaryKeyConstraint,
|
||
SmallInteger,
|
||
String,
|
||
Text,
|
||
UniqueConstraint,
|
||
func,
|
||
text,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
|
||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||
|
||
|
||
class Base(DeclarativeBase):
|
||
"""Declarative base; ``Base.metadata`` is Alembic's ``target_metadata``."""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# identity 域
|
||
# ---------------------------------------------------------------------------
|
||
class User(Base):
|
||
__tablename__ = "id_user"
|
||
|
||
user_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||
nickname: Mapped[str | None] = mapped_column(String(64))
|
||
avatar_key: Mapped[str | None] = mapped_column(String(255))
|
||
register_source: Mapped[str] = mapped_column(String(32), nullable=False, default="unknown")
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
last_active_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
__table_args__ = (Index("ix_id_user_status", "status"),)
|
||
|
||
|
||
class UserIdentity(Base):
|
||
"""External identity mapping. UNIQUE(provider, union_id) per 架构文档 §6.2."""
|
||
|
||
__tablename__ = "id_user_identity"
|
||
|
||
identity_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
union_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
open_id: Mapped[str | None] = mapped_column(String(128))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("provider", "union_id", name="uq_id_user_identity_provider_union"),
|
||
Index("ix_id_user_identity_user", "user_id"),
|
||
)
|
||
|
||
|
||
class Consent(Base):
|
||
"""CR-01 留证:人脸的单独同意必须独立成行,不得混入总协议。"""
|
||
|
||
__tablename__ = "id_consent"
|
||
|
||
consent_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
consent_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
doc_version: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
granted_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
client_ip: Mapped[str | None] = mapped_column(String(64))
|
||
ua: Mapped[str | None] = mapped_column(String(512))
|
||
|
||
__table_args__ = (Index("ix_id_consent_user_type", "user_id", "consent_type"),)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# billing 域(V1:配额与计量,无支付)
|
||
# ---------------------------------------------------------------------------
|
||
class QuotaRule(Base):
|
||
"""FR-BILL-04 配额规则配置化:改配置不发版。"""
|
||
|
||
__tablename__ = "bl_quota_rule"
|
||
|
||
quota_key: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
period_type: Mapped[str] = mapped_column(String(16), nullable=False) # day/month/lifetime
|
||
base_limit: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
task_bonus_max: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
hard_cap: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(String(255))
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
|
||
class Quota(Base):
|
||
"""周期配额计量:按 ``period_key`` 自然重置,无需定时任务清零。"""
|
||
|
||
__tablename__ = "bl_quota"
|
||
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
quota_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
period_key: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
used: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
# 本周期创建时从规则拷下的 base_limit。规则中途被改,本周期仍按快照判定。
|
||
limit_value: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
bonus_granted: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
PrimaryKeyConstraint("user_id", "quota_key", "period_key", name="pk_bl_quota"),
|
||
)
|
||
|
||
|
||
class Ledger(Base):
|
||
"""流水是真相,余额是缓存 (架构文档 §6.1 原则 4 / §6.2 bl_ledger).
|
||
|
||
``tx_type`` 在 V1 只用 system_grant / consume / refund;V2 增加
|
||
purchase / gift —— 这是 V1→V2 免数据迁移的关键,不要拆表。
|
||
``status`` 承载 §7.4 的预扣三段式:reserved → committed / refunded。
|
||
"""
|
||
|
||
__tablename__ = "bl_ledger"
|
||
|
||
tx_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
delta: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
tx_type: Mapped[str] = mapped_column(String(24), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="committed")
|
||
reason: Mapped[str | None] = mapped_column(String(128))
|
||
ref_type: Mapped[str | None] = mapped_column(String(32))
|
||
ref_id: Mapped[str | None] = mapped_column(String(64))
|
||
quota_key: Mapped[str | None] = mapped_column(String(64))
|
||
period_key: Mapped[str | None] = mapped_column(String(32))
|
||
balance_after: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("idempotency_key", name="uq_bl_ledger_idempotency_key"),
|
||
Index("ix_bl_ledger_user_created", "user_id", "created_at"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# task 域(统一任务中心)
|
||
# ---------------------------------------------------------------------------
|
||
class Task(Base):
|
||
"""全系统唯一的长耗时任务视图,前端任务中心直接读它 (架构文档 §6.2)。"""
|
||
|
||
__tablename__ = "tk_task"
|
||
|
||
task_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
task_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
progress: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
stage_text: Mapped[str | None] = mapped_column(String(128))
|
||
idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
result: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
error_code: Mapped[int | None] = mapped_column(Integer)
|
||
error_message: Mapped[str | None] = mapped_column(String(512))
|
||
retry_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
quota_reserved: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
reserve_tx_id: Mapped[int | None] = mapped_column(BigInteger)
|
||
submitted_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
eta_seconds: Mapped[int | None] = mapped_column(Integer)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("idempotency_key", name="uq_tk_task_idempotency_key"),
|
||
Index("ix_tk_task_user_status", "user_id", "status"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 观测与成本
|
||
# ---------------------------------------------------------------------------
|
||
class AiCall(Base):
|
||
"""成本归因的唯一数据源 (架构文档 §13, FR-BILL-08).
|
||
|
||
**不允许「先上线后补埋点」** —— V1 不收费,成本是唯一财务变量,且这张表
|
||
是 V2 定价的唯一数据来源。每一次 provider 调用(含失败与结构化重试)都写
|
||
一行。生产环境按月分区。
|
||
"""
|
||
|
||
__tablename__ = "ob_ai_call"
|
||
|
||
call_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int | None] = mapped_column(BigInteger)
|
||
scene: Mapped[str] = mapped_column(String(48), nullable=False)
|
||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
model: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
# Prompt 版本:架构文档 §8.1「可复现」要求记录,§6.2 的列清单漏了,这里补上。
|
||
prompt_id: Mapped[str | None] = mapped_column(String(64))
|
||
prompt_version: Mapped[str | None] = mapped_column(String(16))
|
||
tokens_in: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
tokens_out: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
tokens_cached: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
# 思考模式 token 单独记,不并入 tokens_out:它按输出价计费,混在一起就看不出
|
||
# 开关思考模式的成本差异,而这正是 T-P1-24a 要长期盯的数。
|
||
tokens_reasoning: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
cost_cents: Mapped[Decimal] = mapped_column(
|
||
Numeric(14, 6), nullable=False, default=Decimal("0")
|
||
)
|
||
# 该次调用适用的峰谷档位。没有它就无法事后复核 cost_cents ——
|
||
# 单价随时刻变化,事后按「当前价」回算会得出不同结果 (T-P1-25c)。
|
||
rate_tier: Mapped[str | None] = mapped_column(String(16))
|
||
success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
error_code: Mapped[str | None] = mapped_column(String(64))
|
||
error_message: Mapped[str | None] = mapped_column(Text)
|
||
ref_type: Mapped[str | None] = mapped_column(String(32))
|
||
ref_id: Mapped[str | None] = mapped_column(String(64))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_ob_ai_call_created", "created_at"),
|
||
Index("ix_ob_ai_call_scene_created", "scene", "created_at"),
|
||
Index("ix_ob_ai_call_user_created", "user_id", "created_at"),
|
||
)
|
||
|
||
|
||
class UserSession(Base):
|
||
"""refresh token 的服务端会话(架构文档 §6.2 id_session,FR-ACC-05).
|
||
|
||
刻意叫 UserSession 而不是 Session:避免与 SQLAlchemy 的 AsyncSession 混淆。
|
||
refresh token 本体是 opaque 随机串(不出现在任何存储里),库里只存 sha256
|
||
哈希 —— 泄漏数据库不等于泄漏令牌。轮换 = 旧行 revoked + 新行,同一事务。
|
||
"""
|
||
|
||
__tablename__ = "id_session"
|
||
|
||
session_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
refresh_token_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||
revoked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("refresh_token_hash", name="uq_id_session_token_hash"),
|
||
Index("ix_id_session_user", "user_id"),
|
||
)
|
||
|
||
|
||
class ObEvent(Base):
|
||
"""客户端埋点事件(架构文档 §6.2 ob_event,FR-SYS-06).
|
||
|
||
架构原文「按月分区」—— V1 先普通表(与 ob_ai_call 同样处理),
|
||
数据量上来后再分区,先不预支复杂度。
|
||
"""
|
||
|
||
__tablename__ = "ob_event"
|
||
|
||
event_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
event_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
props: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
client_ts: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
server_ts: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_ob_event_name_ts", "event_name", "server_ts"),
|
||
Index("ix_ob_event_user_ts", "user_id", "server_ts"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# kb 域(知识库,架构文档 §6.2)
|
||
# ---------------------------------------------------------------------------
|
||
class KbEntity(Base):
|
||
"""12 类实体统一表:attrs JSONB 承载类目差异化字段(架构 §6.2).
|
||
|
||
状态机 draft→reviewing→published→archived 由 service 层校验;
|
||
published 实体不可直接改(version 递增需回 draft 重走评审)。
|
||
"""
|
||
|
||
__tablename__ = "kb_entity"
|
||
|
||
entity_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
entity_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
attrs: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="draft")
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
source: Mapped[str | None] = mapped_column(String(255))
|
||
license: Mapped[str | None] = mapped_column(String(64))
|
||
updated_by: Mapped[str | None] = mapped_column(String(64))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("entity_type", "name", name="uq_kb_entity_type_name"),
|
||
Index("ix_kb_entity_type_status", "entity_type", "status"),
|
||
)
|
||
|
||
|
||
class KbRelation(Base):
|
||
"""邻接表 + 递归 CTE 做 1~3 跳(TD-09:不上 Neo4j)."""
|
||
|
||
__tablename__ = "kb_relation"
|
||
|
||
relation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
from_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
rel_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
to_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
weight: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False, default=Decimal("1"))
|
||
confidence: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False, default=Decimal("1"))
|
||
evidence: Mapped[str | None] = mapped_column(String(255))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("from_id", "rel_type", "to_id", name="uq_kb_relation_triple"),
|
||
Index("ix_kb_relation_to", "to_id"),
|
||
)
|
||
|
||
|
||
class KbRule(Base):
|
||
"""决策表(TD-10 自研规则引擎)。conditions/effects 为 JSONB DSL."""
|
||
|
||
__tablename__ = "kb_rule"
|
||
|
||
rule_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
rule_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
category: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="draft")
|
||
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=60)
|
||
conditions: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
effects: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
is_hard: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
confidence: Mapped[Decimal] = mapped_column(
|
||
Numeric(4, 3), nullable=False, default=Decimal("0.9")
|
||
)
|
||
evidence: Mapped[str | None] = mapped_column(String(255))
|
||
author: Mapped[str | None] = mapped_column(String(64))
|
||
reviewer: Mapped[str | None] = mapped_column(String(64))
|
||
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("rule_key", name="uq_kb_rule_key"),
|
||
Index("ix_kb_rule_status_priority", "status", "priority"),
|
||
)
|
||
|
||
|
||
class KbChunk(Base):
|
||
"""向量 + 全文双索引,支撑混合检索(架构 §8.3).
|
||
|
||
embedding_model/version 必填:没有版本号的向量不可重跑(T-P2-03)。
|
||
tsv 是生成列——写入时不要手工填。
|
||
"""
|
||
|
||
__tablename__ = "kb_chunk"
|
||
|
||
chunk_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
entity_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
chunk_type: Mapped[str] = mapped_column(String(32), nullable=False, default="text")
|
||
text: Mapped[str] = mapped_column(Text, nullable=False)
|
||
embedding = mapped_column(Vector(1024), nullable=False)
|
||
embedding_model: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
embedding_version: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
tsv = mapped_column(
|
||
TSVECTOR, Computed("to_tsvector('simple', text)", persisted=True), nullable=False
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (Index("ix_kb_chunk_entity", "entity_id"),)
|
||
|
||
|
||
class KbAsset(Base):
|
||
"""图像资产。tryon_ready 是换装闭环的关键标志位(架构 §6.2)."""
|
||
|
||
__tablename__ = "kb_asset"
|
||
|
||
asset_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
entity_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
asset_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
cos_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
spec_check: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
tryon_ready: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
review_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
|
||
license: Mapped[str | None] = mapped_column(String(64))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_kb_asset_entity", "entity_id"),
|
||
Index("ix_kb_asset_tryon_ready", "tryon_ready", postgresql_where=text("tryon_ready")),
|
||
)
|
||
|
||
|
||
class KbColor(Base):
|
||
"""自建色板(TD-18:规避「四季色彩/CMB」商标)。Lab 用于 ΔE 数值匹配."""
|
||
|
||
__tablename__ = "kb_color"
|
||
|
||
color_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
season_code: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
role: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
lab_l: Mapped[Decimal] = mapped_column(Numeric(6, 2), nullable=False)
|
||
lab_a: Mapped[Decimal] = mapped_column(Numeric(6, 2), nullable=False)
|
||
lab_b: Mapped[Decimal] = mapped_column(Numeric(6, 2), nullable=False)
|
||
hex: Mapped[str] = mapped_column(String(7), nullable=False)
|
||
name_cn: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
||
__table_args__ = (Index("ix_kb_color_season", "season_code"),)
|
||
|
||
|
||
class KbGap(Base):
|
||
"""知识缺口埋点:无检索结果的查询,驱动内容生产(T-P2-06)."""
|
||
|
||
__tablename__ = "kb_gap"
|
||
|
||
gap_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
query_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||
filter_ctx: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
hit_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
first_seen_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
last_seen_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (Index("ix_kb_gap_query", "query_text"),)
|