b50495bebe
短信(SMS_MOCK 切 mock/real): - integrations/sms.py 重写: real 模式走极光短信 REST /v1/messages 自定义验证码(本服务 secrets 生成 6 位码 + 进程内存 + 本地校验一次性/防爆破), 鉴权复用极光一键登录 JG_APP_KEY/MASTER_SECRET (同一极光应用, 上线只需 SMS_MOCK=false); mock 仍"任意6位通过"不动其余测试 - 防刷四层: 单号冷却 + 单号每日上限 + 单IP rate_limit(/sms/send 10/min、/sms/login 20/min) + 单码失败次数作废; SmsError 带 status_code 映射 429/503/400 - config 增 SMS_SEND_ENDPOINT/SIGN_ID/TEMPLATE_ID/CODE_LENGTH/DAILY_LIMIT/MAX_VERIFY_ATTEMPTS; test_auth 加 real 模式单测; sms.md/后端技术实现/待办账本同步 admin 后台(app/admin/ 独立子应用, uvicorn app.admin.main:admin_app :8771): - 复用主仓 models/repositories/integrations + 同库, 鉴权完全隔离(ADMIN_JWT_SECRET≠JWT_SECRET_KEY + payload typ=admin + bcrypt 密码 + 可选 IP 白名单); 主 app 不 import 本包, admin 崩不影响主进程 - 路由: 登录 / 账号管理(RBAC: super_admin·finance·operator) / 用户列表+360详情+封禁+手动调币 / 钱包流水 / 提现重试对账 / 反馈工单 / 数据大盘; 全写操作落 admin_audit_log(涉钱与业务写同事务) - 涉钱逻辑(调微信/退款/对账)复用 app.repositories.wallet 不重写 - 新增 models/admin.py(AdminUser/AdminAuditLog) + admin_tables 迁移 + create_admin.py + deploy/shaguabijia-admin.service; 依赖加 bcrypt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
66 lines
3.1 KiB
Python
66 lines
3.1 KiB
Python
"""admin_user + admin_audit_log tables (运营 admin 后台:账号 + 操作审计)
|
|
|
|
Revision ID: ad60a1b2c3d4
|
|
Revises: 8ac524a8ea02
|
|
Create Date: 2026-06-03 10:00:00.000000
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = 'ad60a1b2c3d4'
|
|
down_revision: Union[str, Sequence[str], None] = '8ac524a8ea02'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'admin_user',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('username', sa.String(length=64), nullable=False),
|
|
sa.Column('password_hash', sa.String(length=255), nullable=False),
|
|
sa.Column('role', sa.String(length=20), nullable=False, server_default='operator'),
|
|
sa.Column('status', sa.String(length=20), nullable=False, server_default='active'),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
)
|
|
with op.batch_alter_table('admin_user', schema=None) as batch_op:
|
|
batch_op.create_index(batch_op.f('ix_admin_user_username'), ['username'], unique=True)
|
|
|
|
op.create_table(
|
|
'admin_audit_log',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('admin_id', sa.Integer(), nullable=False),
|
|
sa.Column('admin_username', sa.String(length=64), nullable=False),
|
|
sa.Column('action', sa.String(length=64), nullable=False),
|
|
sa.Column('target_type', sa.String(length=32), nullable=False),
|
|
sa.Column('target_id', sa.String(length=64), nullable=True),
|
|
sa.Column('detail', sa.JSON().with_variant(sa.dialects.postgresql.JSONB(), 'postgresql'), nullable=True),
|
|
sa.Column('ip', sa.String(length=64), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
sa.ForeignKeyConstraint(['admin_id'], ['admin_user.id'], ),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
)
|
|
with op.batch_alter_table('admin_audit_log', schema=None) as batch_op:
|
|
batch_op.create_index(batch_op.f('ix_admin_audit_log_admin_id'), ['admin_id'], unique=False)
|
|
batch_op.create_index(batch_op.f('ix_admin_audit_log_action'), ['action'], unique=False)
|
|
batch_op.create_index(batch_op.f('ix_admin_audit_log_created_at'), ['created_at'], unique=False)
|
|
|
|
|
|
def downgrade() -> None:
|
|
with op.batch_alter_table('admin_audit_log', schema=None) as batch_op:
|
|
batch_op.drop_index(batch_op.f('ix_admin_audit_log_created_at'))
|
|
batch_op.drop_index(batch_op.f('ix_admin_audit_log_action'))
|
|
batch_op.drop_index(batch_op.f('ix_admin_audit_log_admin_id'))
|
|
op.drop_table('admin_audit_log')
|
|
|
|
with op.batch_alter_table('admin_user', schema=None) as batch_op:
|
|
batch_op.drop_index(batch_op.f('ix_admin_user_username'))
|
|
op.drop_table('admin_user')
|