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>
71 lines
3.2 KiB
Python
71 lines
3.2 KiB
Python
"""Admin 后台:管理员账号 + 操作审计日志。
|
|
|
|
与 App 用户(user 表)完全隔离:admin 走独立 JWT secret、独立鉴权链(见 app/admin/)。
|
|
- admin_user:账号密码(bcrypt)登录,带角色(super_admin / finance / operator)。
|
|
- admin_audit_log:每个写操作落一条,记前后值,不可删,用于追溯"谁在何时改了谁的钱/状态"。
|
|
admin_username / target_id 冗余存字符串,即使关联对象被删/改名也能追溯。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, func
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.db.base import Base
|
|
|
|
# PG 用 JSONB,SQLite(本地/测试)退化为通用 JSON(同 comparison_record.raw_payload)。
|
|
_JSON = JSON().with_variant(JSONB(), "postgresql")
|
|
|
|
|
|
class AdminUser(Base):
|
|
__tablename__ = "admin_user"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
username: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
# super_admin(全权+管账号)/ finance(钱:提现+金币)/ operator(用户+反馈+大盘)
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False, default="operator")
|
|
# active / disabled
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active")
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
last_login_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
def __repr__(self) -> str: # pragma: no cover
|
|
return f"<AdminUser id={self.id} username={self.username} role={self.role}>"
|
|
|
|
|
|
class AdminAuditLog(Base):
|
|
__tablename__ = "admin_audit_log"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
admin_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("admin_user.id"), index=True, nullable=False
|
|
)
|
|
# 冗余存操作者用户名:admin 改名/禁用后仍可追溯是谁干的
|
|
admin_username: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
# 操作类型,如 user.coins.grant / user.status.set / withdraw.refresh / feedback.handle
|
|
action: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
|
|
# 被操作对象类型 + id(id 用字符串以兼容 out_bill_no 等非整型主键)
|
|
target_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
target_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
# 上下文 + 前后值,如 {"amount": 1000, "reason": "...", "before": {...}, "after": {...}}
|
|
detail: Mapped[dict | None] = mapped_column(_JSON, nullable=True)
|
|
ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
|
|
)
|
|
|
|
def __repr__(self) -> str: # pragma: no cover
|
|
return (
|
|
f"<AdminAuditLog id={self.id} admin={self.admin_username} "
|
|
f"action={self.action} target={self.target_type}:{self.target_id}>"
|
|
)
|