0717c09721
改动:新增厂商推送配置、设备 push_vendor/push_token 字段、device push-test 接口、心跳超时厂商直推发送逻辑和对应测试。 验证:python -m pytest tests/test_device_push.py tests/test_auth.py tests/test_health.py 通过。 --------- Co-authored-by: guke <guke@wonderable.ai> Co-authored-by: 左辰勇 <exinglang@gmail.com> Co-authored-by: lowmaster-chen <1119780489@qq.com> Reviewed-on: #118 Co-authored-by: Ghost <> Co-committed-by: Ghost <>
69 lines
3.2 KiB
Python
69 lines
3.2 KiB
Python
"""notification table (消息通知中心 站内消息)
|
|
|
|
Revision ID: notification_table
|
|
Revises: 1a924c274fce
|
|
Create Date: 2026-07-15 12:00:00.000000
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = 'notification_table'
|
|
down_revision: Union[str, Sequence[str], None] = '1a924c274fce'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
# PG 用 JSONB,SQLite 退化为通用 JSON(与 models/notification._JSON 一致)。
|
|
_JSON = sa.JSON().with_variant(postgresql.JSONB(), 'postgresql')
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'notification',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
|
sa.Column('type', sa.String(length=32), nullable=False),
|
|
sa.Column('coins', sa.Integer(), nullable=True),
|
|
sa.Column('cash_cents', sa.Integer(), nullable=True),
|
|
sa.Column('info_rows', _JSON, nullable=False),
|
|
sa.Column('extra', _JSON, nullable=False),
|
|
sa.Column('is_read', sa.Boolean(), nullable=False),
|
|
sa.Column('read_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('dedup_key', sa.String(length=64), nullable=True),
|
|
sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
)
|
|
with op.batch_alter_table('notification', schema=None) as batch_op:
|
|
batch_op.create_index('ix_notification_type', ['type'], unique=False)
|
|
# 列表分页:按用户取 + sent_at 倒序
|
|
batch_op.create_index('ix_notification_user_sent', ['user_id', 'sent_at'], unique=False)
|
|
# 铃铛角标:count where user_id=? and is_read=false —— 部分索引只覆盖未读行
|
|
batch_op.create_index(
|
|
'ix_notification_user_unread', ['user_id'], unique=False,
|
|
sqlite_where=sa.text('is_read = 0'),
|
|
postgresql_where=sa.text('is_read = false'),
|
|
)
|
|
# 去重/合并:同一 (user, type, dedup_key) 未读期间只允许一条(已读后可再生成)
|
|
batch_op.create_index(
|
|
'uq_notification_user_type_dedup', ['user_id', 'type', 'dedup_key'], unique=True,
|
|
sqlite_where=sa.text('dedup_key IS NOT NULL AND is_read = 0'),
|
|
postgresql_where=sa.text('dedup_key IS NOT NULL AND is_read = false'),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
with op.batch_alter_table('notification', schema=None) as batch_op:
|
|
batch_op.drop_index('uq_notification_user_type_dedup')
|
|
batch_op.drop_index('ix_notification_user_unread')
|
|
batch_op.drop_index('ix_notification_user_sent')
|
|
batch_op.drop_index('ix_notification_type')
|
|
op.drop_table('notification')
|