Initial project commit

This commit is contained in:
2026-05-09 16:40:29 +08:00
commit 02b0259a9e
267 changed files with 54891 additions and 0 deletions
View File
+29
View File
@@ -0,0 +1,29 @@
import json
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.audit_log import AuditLog
async def log_action(
db: AsyncSession,
operator_id: int,
action: str,
target_type: str,
target_id: int | None = None,
detail: dict | str | None = None,
) -> AuditLog:
detail_str = None
if detail is not None:
detail_str = json.dumps(detail, ensure_ascii=False) if isinstance(detail, dict) else str(detail)
entry = AuditLog(
operator_id=operator_id,
action=action,
target_type=target_type,
target_id=target_id,
detail=detail_str,
)
db.add(entry)
await db.flush()
return entry
+65
View File
@@ -0,0 +1,65 @@
import logging
import re
from pathlib import Path
logger = logging.getLogger(__name__)
_SENSITIVE_WORDS: list[str] = []
_PATTERN: re.Pattern | None = None
def _load_words():
global _SENSITIVE_WORDS, _PATTERN
words_file = Path(__file__).parent.parent.parent / "sensitive_words.txt"
if words_file.exists():
raw = words_file.read_text(encoding="utf-8")
_SENSITIVE_WORDS = [w.strip() for w in raw.splitlines() if w.strip()]
else:
_SENSITIVE_WORDS = []
if _SENSITIVE_WORDS:
escaped = [re.escape(w) for w in _SENSITIVE_WORDS]
_PATTERN = re.compile("|".join(escaped), re.IGNORECASE)
else:
_PATTERN = None
_load_words()
def reload_sensitive_words():
"""Hot-reload the sensitive word list from disk."""
_load_words()
logger.info("Reloaded %d sensitive words", len(_SENSITIVE_WORDS))
def check_text(text: str) -> dict:
"""
Check text against the sensitive word list.
Returns {"safe": True/False, "matched": [...]}
"""
if not text or _PATTERN is None:
return {"safe": True, "matched": []}
matches = _PATTERN.findall(text)
if matches:
unique = list(set(matches))
return {"safe": False, "matched": unique}
return {"safe": True, "matched": []}
def filter_text(text: str, replacement: str = "**") -> str:
"""Replace sensitive words with the replacement string."""
if not text or _PATTERN is None:
return text
return _PATTERN.sub(replacement, text)
async def check_image_safety(image_url: str) -> dict:
"""
Placeholder for third-party image audit.
In production, integrate with Tencent Cloud / Aliyun content moderation API.
Returns {"safe": True/False, "labels": [...]}
"""
logger.debug("Image safety check (placeholder): %s", image_url)
return {"safe": True, "labels": []}
@@ -0,0 +1,25 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.notification import Notification
async def send_notification(
db: AsyncSession,
user_id: int,
type: str,
title: str,
content: str | None = None,
ref_type: str | None = None,
ref_id: int | None = None,
):
n = Notification(
user_id=user_id,
type=type,
title=title,
content=content,
ref_type=ref_type,
ref_id=ref_id,
)
db.add(n)
await db.flush()
return n
+34
View File
@@ -0,0 +1,34 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.point_ledger import PointLedger
async def grant_points(
db: AsyncSession,
user_id: int,
change: int,
reason: str,
ref_type: str | None = None,
ref_id: int | None = None,
) -> PointLedger:
last_entry = await db.execute(
select(PointLedger)
.where(PointLedger.user_id == user_id)
.order_by(PointLedger.id.desc())
.limit(1)
)
last = last_entry.scalar_one_or_none()
current_balance = last.balance if last else 0
ledger = PointLedger(
user_id=user_id,
change=change,
balance=current_balance + change,
reason=reason,
ref_type=ref_type,
ref_id=ref_id,
)
db.add(ledger)
await db.flush()
return ledger