Files
2026-10-16 13:51:00 +08:00

146 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""通用服务:版本检查 / 通知 / 举报(对应《03-接口文档》第 10 章)"""
import json
from datetime import datetime
import redis.asyncio as aioredis
from sqlalchemy import func, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.core import errors, ratelimit
from app.core.response import BizException
from app.models.common import Notification, Report
from app.services import config_service
# 举报限流:10 次/天/用户
REPORT_DAILY_LIMIT = 10
def _fmt(dt: datetime | None) -> str | None:
return dt.strftime("%Y-%m-%d %H:%M:%S") if dt else None
# ---------- 版本检查 ----------
async def get_app_version(db: AsyncSession, redis: aioredis.Redis, platform: str, version_code: int) -> dict:
"""版本检查(10.1):配置存 system_configs key=app.version.{platform},JSON 值,后台可改"""
raw = await config_service.get_config(db, redis, f"app.version.{platform}", "")
cfg: dict = {}
if raw:
try:
cfg = json.loads(raw)
except (TypeError, ValueError):
cfg = {}
min_code = int(cfg.get("min_version_code", 0))
return {
"latest_version": cfg.get("latest_version", "1.0.0"),
"latest_version_code": int(cfg.get("latest_version_code", 1)),
"force_update": version_code < min_code,
"update_url": cfg.get("update_url", ""),
"notice": cfg.get("notice", ""),
}
# ---------- 通知 ----------
async def notify(
db: AsyncSession, redis: aioredis.Redis, user_id: int,
type: int, title: str, content: str = "", biz_id: str = "",
) -> None:
"""写入通知并累加 Redis 未读数(审核/提现/公告/评论回复共用,供后续模块调用)"""
db.add(Notification(user_id=user_id, type=type, title=title, content=content, biz_id=biz_id))
await db.flush()
await redis.incr(f"notif:unread:{user_id}")
async def list_notifications(
db: AsyncSession, user_id: int, page: int, page_size: int, type: int | None
) -> dict:
"""通知列表(10.2)"""
stmt = select(Notification).where(Notification.user_id == user_id)
if type is not None:
stmt = stmt.where(Notification.type == type)
total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar() or 0
rows = (
await db.execute(
stmt.order_by(Notification.id.desc()).offset((page - 1) * page_size).limit(page_size)
)
).scalars().all()
return {
"total": total,
"list": [
{
"id": n.id,
"type": n.type,
"title": n.title,
"content": n.content,
"biz_id": n.biz_id,
"is_read": bool(n.is_read),
"created_at": _fmt(n.created_at),
}
for n in rows
],
}
async def get_unread_count(db: AsyncSession, redis: aioredis.Redis, user_id: int) -> int:
"""未读数(10.3):直接读 Redis;缺失时从 DB 回填"""
key = f"notif:unread:{user_id}"
cached = await redis.get(key)
if cached is not None:
return max(int(cached), 0)
count = (await db.execute(
select(func.count(Notification.id)).where(Notification.user_id == user_id, Notification.is_read == 0)
)).scalar() or 0
await redis.set(key, count)
return count
async def mark_read(
db: AsyncSession, redis: aioredis.Redis, user_id: int, ids: list[int] | None, mark_all: bool
) -> None:
"""标记已读(10.4):ids 与 all 二选一;同步修正 Redis 未读数"""
if mark_all:
await db.execute(
update(Notification).where(Notification.user_id == user_id, Notification.is_read == 0)
.values(is_read=1)
)
await db.commit()
await redis.set(f"notif:unread:{user_id}", 0)
return
if not ids:
raise BizException(errors.PARAM_ERROR, "ids 与 all 需传其一")
result = await db.execute(
update(Notification)
.where(Notification.user_id == user_id, Notification.is_read == 0, Notification.id.in_(ids))
.values(is_read=1)
)
await db.commit()
affected = result.rowcount or 0
if affected > 0:
# 未读数下限 0(DB 为准重算,避免并发脏读)
remaining = (await db.execute(
select(func.count(Notification.id)).where(Notification.user_id == user_id, Notification.is_read == 0)
)).scalar() or 0
await redis.set(f"notif:unread:{user_id}", remaining)
# ---------- 举报 ----------
async def create_report(db: AsyncSession, redis: aioredis.Redis, user_id: int, req) -> None:
"""提交举报(10.5):唯一索引 uk_user_target 防重复(2106);限流 10 次/天"""
today = f"{datetime.now():%Y%m%d}"
await ratelimit.hit(redis, f"rl:report:{user_id}:{today}", REPORT_DAILY_LIMIT, 24 * 3600)
db.add(Report(
user_id=user_id,
target_type=req.target_type,
target_id=req.target_id,
reason_type=req.reason_type,
remark=req.remark or "",
))
try:
await db.commit()
except IntegrityError:
await db.rollback()
raise BizException(errors.REPORT_DUPLICATED)