ComfyUI-Ranking-API / comment_store.py
ZHIWEI666's picture
Upload 91 files
b9d11ff verified
Raw History Blame Contribute Delete
8.67 kB
"""评论记录的锁内修改;路由负责认证、内容存在性及响应契约。"""
import copy
import asyncio
import logging
import threading
from owned_thread import run_owned_thread
from fastapi import HTTPException
import 数据库连接 as db
from resource_deletion import resource_mutation_lock, assert_comment_target_active
from resource_store import ResourceStoreError
COMMENT_MAX_LENGTH = 5000
MAX_COMMENT_READS = 8
_comment_read_capacity = threading.BoundedSemaphore(MAX_COMMENT_READS)
_comment_read_gate = threading.Lock()
_comment_read_accepting = True
_comment_read_pending = 0
_comment_read_closers = 0
_comment_read_drained = threading.Event()
_comment_read_drained.set()
def start_comment_reads():
global _comment_read_accepting
with _comment_read_gate:
if _comment_read_pending or _comment_read_closers:
raise RuntimeError("comment reads are still pending")
_comment_read_accepting = True
def stop_comment_reads():
global _comment_read_accepting
with _comment_read_gate:
_comment_read_accepting = False
async def close_comment_reads():
global _comment_read_accepting, _comment_read_closers
with _comment_read_gate:
_comment_read_accepting = False
_comment_read_closers += 1
try:
cancelled = False
while not _comment_read_drained.is_set():
try:
await asyncio.sleep(.01)
except asyncio.CancelledError:
cancelled = True
if cancelled:
raise asyncio.CancelledError
finally:
with _comment_read_gate:
_comment_read_closers -= 1
def _item_comments(data, item_id):
if not isinstance(data, dict):
raise HTTPException(status_code=503, detail="评论数据暂不可用")
records = data.get(item_id, [])
if not isinstance(records, list):
raise HTTPException(status_code=503, detail="评论数据暂不可用")
return records
def append_comment(item_id, record, parent_id=None, prepend=False):
def updater(data):
records = _item_comments(data, item_id)
if parent_id:
parent = next((entry for entry in records if isinstance(entry, dict)
and entry.get("id") == parent_id), None)
if parent is None:
raise HTTPException(status_code=404, detail="找不到要回复的评论")
replies = parent.setdefault("replies", [])
if not isinstance(replies, list):
raise HTTPException(status_code=503, detail="评论数据暂不可用")
replies.append(copy.deepcopy(record))
elif prepend:
records.insert(0, copy.deepcopy(record))
else:
records.append(copy.deepcopy(record))
data[item_id] = records
try:
with resource_mutation_lock:
assert_comment_target_active(db, item_id)
db.atomic_update("comments.json", updater, default_data={})
except ResourceStoreError as error:
raise HTTPException(status_code=error.status_code, detail=error.message) from None
def _find_comment(records, comment_id, depth=0):
if depth > 20 or not isinstance(records, list):
return None
for record in records:
if not isinstance(record, dict):
continue
if record.get("id") == comment_id:
return record
found = _find_comment(record.get("replies", []), comment_id, depth + 1)
if found is not None:
return found
return None
def delete_comment(item_id, comment_id, account, can_moderate=False):
def updater(data):
target = _find_comment(_item_comments(data, item_id), comment_id)
if target is None:
raise HTTPException(status_code=404, detail="找不到该评论")
# 在同一锁内检查最新目标并执行软删除,不能使用先前读取的快照。
if target.get("author") != account and not can_moderate:
raise HTTPException(status_code=403, detail="无权删除此评论")
target["isDeleted"] = True
target["content"] = ""
db.atomic_update("comments.json", updater, default_data={})
def increment_comment_count(file_name, item_id):
"""派生计数独立更新;失败不能把已提交评论误报为失败或重放评论。"""
def updater(records):
for record in records:
if record.get("id") == item_id:
old_count = record.get("comments", 0)
record["comments"] = (old_count if type(old_count) is int and old_count >= 0 else 0) + 1
break
try:
db.atomic_update(file_name, updater, default_data=[])
return True
except Exception:
logging.getLogger(__name__).warning("评论已提交,派生计数暂未更新")
return False
def _visible_count(records, depth=0):
if not isinstance(records, list) or depth > 20:
raise ValueError("Invalid comment thread")
count = 0
for record in records:
if not isinstance(record, dict) or not isinstance(record.get("id"), str) or not record["id"].strip():
raise ValueError("Invalid comment record")
deleted = record.get("isDeleted", False)
if type(deleted) is not bool:
raise ValueError("Invalid comment state")
if not deleted:
count += 1
count += _visible_count(record.get("replies", []), depth + 1)
return count
def _validate_comment_text(records):
for record in records:
if not isinstance(record.get("author"), str) or not isinstance(record.get("content"), str):
raise ValueError("Invalid comment text")
_validate_comment_text(record.get("replies", []))
def read_comment_thread(item_id):
"""严格读取评论快照;存储故障不冒充成功空列表,不改历史主记录。"""
try:
snapshot = db.load_data("comments.json", default_data={}, skip_cache=True, strict=True)
records = _item_comments(snapshot, item_id)
_visible_count(records)
_validate_comment_text(records)
return copy.deepcopy(records)
except Exception:
logging.getLogger(__name__).warning("评论列表无法核实")
raise HTTPException(status_code=503, detail="评论暂时无法读取") from None
def _read_comment_view(item_id):
records = read_comment_thread(item_id)
users = db.load_data("users.json", default_data={})
result = []
for record in records:
author_info = users.get(record["author"], {})
result.append({**record, "author_name": author_info.get("name", record["author"]),
"author_avatar": author_info.get("avatarDataUrl", "")})
return result
async def read_comment_view(item_id):
"""整次评论读取在所属后台工作中执行;繁忙不提交线程池队列。"""
global _comment_read_pending
with _comment_read_gate:
if not _comment_read_accepting or not _comment_read_capacity.acquire(blocking=False):
raise HTTPException(status_code=503, detail="评论暂时无法读取")
_comment_read_pending += 1
_comment_read_drained.clear()
try:
return await run_owned_thread(_read_comment_view, item_id)
except HTTPException:
raise
except Exception:
logging.getLogger(__name__).warning("评论展示无法核实")
raise HTTPException(status_code=503, detail="评论暂时无法读取") from None
finally:
with _comment_read_gate:
_comment_read_capacity.release()
_comment_read_pending -= 1
if not _comment_read_pending:
_comment_read_drained.set()
def attach_comment_counts(records):
"""只校对响应副本,以评论主记录恢复展示计数,不写内容或资金数据。"""
try:
snapshot = db.load_data("comments.json", default_data={}, skip_cache=True, strict=True)
if not isinstance(snapshot, dict):
raise ValueError("Invalid comment data")
except Exception:
logging.getLogger(__name__).warning("评论计数无法校对,保留已有计数")
return records
for record in records:
try:
count = _visible_count(snapshot.get(record["id"], []))
except (KeyError, TypeError, ValueError):
continue
record["comments"] = count
return records