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