up
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""通用服务:版本检查 / 通知 / 举报(对应《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)
|
||||
Reference in New Issue
Block a user