feat: 短信接入极光真实发送 + 新增运营 admin 后台子应用
短信(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>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""admin 大盘聚合查询(全局 count/sum/DAU/成功率)。全部只读、不改任何数据。
|
||||
|
||||
⚠️ 性能:这些是全表 count/sum,P0 数据量小够用;用户量上来后热点字段(user.created_at /
|
||||
user.last_login_at / comparison_record.status / withdraw_order.status)要加索引,或改增量统计表。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.comparison import ComparisonRecord
|
||||
from app.models.feedback import Feedback
|
||||
from app.models.user import User
|
||||
from app.models.wallet import CoinTransaction, WithdrawOrder
|
||||
|
||||
_BEIJING = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def _beijing_today_start_utc() -> datetime:
|
||||
"""北京时间今天 0 点对应的 UTC 时刻(DAU / 今日新增按北京时区切天)。"""
|
||||
now_bj = datetime.now(_BEIJING)
|
||||
start_bj = now_bj.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return start_bj.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def dashboard_overview(db: Session) -> dict:
|
||||
today_start = _beijing_today_start_utc()
|
||||
|
||||
def _count(model, *conds) -> int:
|
||||
stmt = select(func.count(model.id))
|
||||
if conds:
|
||||
stmt = stmt.where(*conds)
|
||||
return db.execute(stmt).scalar_one()
|
||||
|
||||
def _sum(col, *conds) -> int:
|
||||
stmt = select(func.coalesce(func.sum(col), 0))
|
||||
if conds:
|
||||
stmt = stmt.where(*conds)
|
||||
return db.execute(stmt).scalar_one()
|
||||
|
||||
# ===== 用户 =====
|
||||
by_status = dict(
|
||||
db.execute(select(User.status, func.count(User.id)).group_by(User.status)).all()
|
||||
)
|
||||
|
||||
# ===== 提现状态分布 =====
|
||||
wd_by_status = dict(
|
||||
db.execute(
|
||||
select(WithdrawOrder.status, func.count(WithdrawOrder.id)).group_by(
|
||||
WithdrawOrder.status
|
||||
)
|
||||
).all()
|
||||
)
|
||||
|
||||
# ===== 比价 =====
|
||||
comparison_total = _count(ComparisonRecord)
|
||||
comparison_success = _count(ComparisonRecord, ComparisonRecord.status == "success")
|
||||
success_rate = round(comparison_success / comparison_total, 4) if comparison_total else 0.0
|
||||
|
||||
return {
|
||||
"users": {
|
||||
"total": _count(User),
|
||||
"active": by_status.get("active", 0),
|
||||
"disabled": by_status.get("disabled", 0),
|
||||
"deleted": by_status.get("deleted", 0),
|
||||
"new_today": _count(User, User.created_at >= today_start),
|
||||
"dau": _count(User, User.last_login_at >= today_start),
|
||||
},
|
||||
"coins": {
|
||||
# 累计发放金币(coin_transaction 里所有 amount>0 之和;负数是兑换/扣减不计)
|
||||
"granted_total": _sum(CoinTransaction.amount, CoinTransaction.amount > 0),
|
||||
},
|
||||
"cash": {
|
||||
"withdraw_success_cents": _sum(
|
||||
WithdrawOrder.amount_cents, WithdrawOrder.status == "success"
|
||||
),
|
||||
"withdraw_pending_count": wd_by_status.get("pending", 0),
|
||||
"withdraw_success_count": wd_by_status.get("success", 0),
|
||||
"withdraw_failed_count": wd_by_status.get("failed", 0),
|
||||
},
|
||||
"comparison": {
|
||||
"total": comparison_total,
|
||||
"success": comparison_success,
|
||||
"success_rate": success_rate,
|
||||
},
|
||||
"feedback": {"new": _count(Feedback, Feedback.status == "new")},
|
||||
# CPS 收入数据源未接(referral-link 只换链接,转化/佣金未回收)→ 前端显示"待接入"。
|
||||
"cps": {"available": False, "note": "CPS 转化数据未接入(P2)"},
|
||||
}
|
||||
Reference in New Issue
Block a user