Spaces:
Running
Running
File size: 8,666 Bytes
c8e5eaa b9d11ff c8e5eaa b9d11ff c8e5eaa b9d11ff c8e5eaa b9d11ff c8e5eaa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | """评论记录的锁内修改;路由负责认证、内容存在性及响应契约。"""
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
|