ZHIWEI666 commited on
Commit
8f0ec20
·
verified ·
1 Parent(s): 9bee095

增加浏览量数据,与原创勾选

Browse files
Files changed (8) hide show
  1. db_utils.py +198 -0
  2. models.py +5 -1
  3. router_comments.py +7 -0
  4. router_items.py +124 -9
  5. router_messages.py +1 -15
  6. router_posts.py +191 -99
  7. router_tasks.py +305 -12
  8. 数据库连接.py +133 -0
db_utils.py CHANGED
@@ -359,6 +359,204 @@ def paginate(
359
  }
360
 
361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  # ==========================================
363
  # 🔄 批量操作工具函数
364
  # ==========================================
 
359
  }
360
 
361
 
362
+ # ==========================================
363
+ # 👁️ 访问量记录工具函数
364
+ # ==========================================
365
+
366
+ def record_view(data_file: str, item_id: str, user_account: str) -> Optional[Dict]:
367
+ """
368
+ 记录访问量(原子操作,并发安全)
369
+
370
+ 参数:
371
+ data_file: JSON 文件名(如 Items.json)
372
+ item_id: 要记录访问的 item/task ID
373
+ user_account: 访问用户账号
374
+
375
+ 返回:
376
+ {"views": N, "daily_views": N} 成功
377
+ None 记录不存在
378
+
379
+ 逻辑:
380
+ - 初始化字段: views=0, viewed_by=[], daily_views=0, daily_views_date=""
381
+ - daily_views 每次调用都增加
382
+ - views 只在用户首次访问时增加(user_account 不在 viewed_by 中)
383
+ - 如果 daily_views_date 不是今天,重置 daily_views=0 并更新日期
384
+
385
+ 并发安全:
386
+ - 使用 atomic_update 确保读-改-写在同一把锁内完成
387
+ - 高并发下不会丢失访问量
388
+ """
389
+ from datetime import date
390
+
391
+ # 用于在闭包中存储结果
392
+ result_container = [None]
393
+
394
+ def updater(data):
395
+ # 查找目标记录
396
+ target_item = None
397
+
398
+ if isinstance(data, dict):
399
+ if item_id in data:
400
+ target_item = data[item_id]
401
+ else:
402
+ for item in data:
403
+ if item.get("id") == item_id:
404
+ target_item = item
405
+ break
406
+
407
+ if target_item is None:
408
+ result_container[0] = None
409
+ return
410
+
411
+ # 初始化字段(如果不存在)
412
+ if "views" not in target_item:
413
+ target_item["views"] = 0
414
+ if "viewed_by" not in target_item:
415
+ target_item["viewed_by"] = []
416
+ if "daily_views" not in target_item:
417
+ target_item["daily_views"] = 0
418
+ if "daily_views_date" not in target_item:
419
+ target_item["daily_views_date"] = ""
420
+
421
+ # 获取今天的日期字符串
422
+ today_str = date.today().isoformat() # "2026-04-02"
423
+
424
+ # 检查是否需要重置日访问量
425
+ if target_item["daily_views_date"] != today_str:
426
+ target_item["daily_views"] = 0
427
+ target_item["daily_views_date"] = today_str
428
+
429
+ # 增加日访问量(每次调用都增加)
430
+ target_item["daily_views"] += 1
431
+
432
+ # 检查用户是否已访问过
433
+ if user_account not in target_item["viewed_by"]:
434
+ target_item["viewed_by"].append(user_account)
435
+ target_item["views"] += 1
436
+
437
+ # 保存结果到闭包容器
438
+ result_container[0] = {
439
+ "views": target_item["views"],
440
+ "daily_views": target_item["daily_views"]
441
+ }
442
+
443
+ # 使用原子更新,整个读-改-写过程在同一把锁内完成
444
+ db.atomic_update(data_file, updater, default_data=[])
445
+
446
+ return result_container[0]
447
+
448
+
449
+ # ==========================================
450
+ # 🗂️ 排序结果缓存工具类
451
+ # ==========================================
452
+
453
+ import time
454
+ from typing import Callable, List, Dict, Any
455
+
456
+ class SortCache:
457
+ """
458
+ 排序结果缓存类 - 用于缓存列表排序结果,减少重复排序开销
459
+
460
+ 设计原则:
461
+ 1. 缓存排序后的 ID 顺序,而非完整数据,避免内存浪费
462
+ 2. 使用 TTL (默认5分钟) 自动过期,确保数据新鲜度
463
+ 3. 写操作时清除相关缓存,确保数据一致性
464
+ 4. 在 load_data 返回的最新数据之上应用缓存的顺序
465
+
466
+ 使用示例:
467
+ # 在列表接口中
468
+ def get_items(sort="time"):
469
+ items = db.load_data("items.json", default_data=[])
470
+ cache_key = f"items:{sort}"
471
+
472
+ def sort_fn(data):
473
+ if sort == "likes":
474
+ data.sort(key=lambda x: x.get("likes", 0), reverse=True)
475
+ else:
476
+ data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
477
+
478
+ return sort_cache.get_sorted(cache_key, items, sort_fn)
479
+
480
+ # 在写操作后
481
+ def create_item(...):
482
+ ...
483
+ sort_cache.invalidate("items")
484
+ """
485
+
486
+ def __init__(self, ttl: int = 300):
487
+ """
488
+ 初始化排序缓存
489
+
490
+ 参数:
491
+ ttl: 缓存过期时间(秒),默认 300 秒(5分钟)
492
+ """
493
+ self._cache: Dict[str, tuple] = {} # {cache_key: (sorted_ids, timestamp)}
494
+ self._ttl = ttl
495
+
496
+ def get_sorted(self, cache_key: str, items: List[Dict], sort_fn: Callable[[List[Dict]], None]) -> List[Dict]:
497
+ """
498
+ 获取排序后的数据(带缓存)
499
+
500
+ 参数:
501
+ cache_key: 缓存键,应包含数据文件和排序参数
502
+ items: 原始数据列表(来自 load_data 的最新数据)
503
+ sort_fn: 排序函数,接收 items 列表并原地排序
504
+
505
+ 返回:
506
+ 排序后的 items 列表
507
+ """
508
+ now = time.time()
509
+
510
+ # 检查缓存是否有效
511
+ if cache_key in self._cache:
512
+ sorted_ids, cached_time = self._cache[cache_key]
513
+ if now - cached_time < self._ttl:
514
+ # 缓存有效,用缓存的顺序重排当前数据
515
+ id_order = {id_: idx for idx, id_ in enumerate(sorted_ids)}
516
+ # 按缓存的顺序排序,新数据(不在缓存中的)放在最后
517
+ return sorted(items, key=lambda x: id_order.get(x.get("id"), float('inf')))
518
+
519
+ # 缓存无效或不存在,执行排序
520
+ sort_fn(items)
521
+
522
+ # 缓存排序后的 ID 列表
523
+ sorted_ids = [item.get("id") for item in items]
524
+ self._cache[cache_key] = (sorted_ids, now)
525
+
526
+ return items
527
+
528
+ def invalidate(self, prefix: str = ""):
529
+ """
530
+ 清除缓存
531
+
532
+ 参数:
533
+ prefix: 缓存键前缀,如果指定则只清除匹配的缓存,否则清除所有缓存
534
+ """
535
+ if prefix:
536
+ keys_to_remove = [k for k in self._cache if k.startswith(prefix)]
537
+ for k in keys_to_remove:
538
+ del self._cache[k]
539
+ else:
540
+ self._cache.clear()
541
+
542
+ def get_stats(self) -> Dict[str, Any]:
543
+ """
544
+ 获取缓存统计信息(用于调试)
545
+ """
546
+ now = time.time()
547
+ valid_count = sum(1 for _, ts in self._cache.values() if now - ts < self._ttl)
548
+ return {
549
+ "total_cached": len(self._cache),
550
+ "valid": valid_count,
551
+ "expired": len(self._cache) - valid_count,
552
+ "ttl": self._ttl
553
+ }
554
+
555
+
556
+ # 全局排序缓存实例(TTL 5分钟)
557
+ sort_cache = SortCache(ttl=300)
558
+
559
+
560
  # ==========================================
561
  # 🔄 批量操作工具函数
562
  # ==========================================
models.py CHANGED
@@ -65,6 +65,7 @@ class ItemCreate(BaseModel):
65
  github_token: Optional[str] = None
66
  netdisk_password: Optional[str] = None # ☁️ 网盘提取码(加密存储,购买后解密)
67
  is_netdisk: Optional[bool] = False # ☁️ 是否为网盘资源
 
68
 
69
  class ItemUpdate(BaseModel):
70
  title: Optional[str] = None
@@ -77,6 +78,7 @@ class ItemUpdate(BaseModel):
77
  github_token: Optional[str] = None
78
  netdisk_password: Optional[str] = None # ☁️ 网盘提取码
79
  is_netdisk: Optional[bool] = None # ☁️ 是否为网盘资源
 
80
 
81
  class FollowToggle(BaseModel):
82
  user_id: str
@@ -211,10 +213,12 @@ class PostCreate(BaseModel):
211
  cover_image: str # 封面图(第一张)
212
  images: Optional[List[str]] = [] # 图片列表(最多9张)
213
  author: str # 作者账号
 
214
 
215
  class PostUpdate(BaseModel):
216
  """ 更新帖子 """
217
  title: Optional[str] = None
218
  content: Optional[str] = None
219
  cover_image: Optional[str] = None
220
- images: Optional[List[str]] = None
 
 
65
  github_token: Optional[str] = None
66
  netdisk_password: Optional[str] = None # ☁️ 网盘提取码(加密存储,购买后解密)
67
  is_netdisk: Optional[bool] = False # ☁️ 是否为网盘资源
68
+ is_original: Optional[bool] = False # 🎨 是否为原创作品
69
 
70
  class ItemUpdate(BaseModel):
71
  title: Optional[str] = None
 
78
  github_token: Optional[str] = None
79
  netdisk_password: Optional[str] = None # ☁️ 网盘提取码
80
  is_netdisk: Optional[bool] = None # ☁️ 是否为网盘资源
81
+ is_original: Optional[bool] = None # 🎨 是否为原创作品
82
 
83
  class FollowToggle(BaseModel):
84
  user_id: str
 
213
  cover_image: str # 封面图(第一张)
214
  images: Optional[List[str]] = [] # 图片列表(最多9张)
215
  author: str # 作者账号
216
+ is_original: Optional[bool] = False # 🎨 是否为原创作品
217
 
218
  class PostUpdate(BaseModel):
219
  """ 更新帖子 """
220
  title: Optional[str] = None
221
  content: Optional[str] = None
222
  cover_image: Optional[str] = None
223
+ images: Optional[List[str]] = None
224
+ is_original: Optional[bool] = None # 🎨 是否为原创作品
router_comments.py CHANGED
@@ -77,6 +77,7 @@ async def soft_delete_comment(item_id: str, comment_id: str, account: str = Depe
77
  items_db = db.load_data("items.json", default_data=[])
78
  posts_db = db.load_data("posts.json", default_data=[])
79
  users_db = db.load_data("users.json", default_data={})
 
80
 
81
  item_comments = comments_db.get(item_id, [])
82
  target_comment = None
@@ -112,6 +113,12 @@ async def soft_delete_comment(item_id: str, comment_id: str, account: str = Depe
112
  if item["id"] == item_id and item.get("author") == account:
113
  is_content_author = True
114
  break
 
 
 
 
 
 
115
  # 检查是否为个人主页留言板作者
116
  if not is_content_author:
117
  if item_id in users_db and item_id == account:
 
77
  items_db = db.load_data("items.json", default_data=[])
78
  posts_db = db.load_data("posts.json", default_data=[])
79
  users_db = db.load_data("users.json", default_data={})
80
+ tasks_db = db.load_data("tasks.json", default_data=[])
81
 
82
  item_comments = comments_db.get(item_id, [])
83
  target_comment = None
 
113
  if item["id"] == item_id and item.get("author") == account:
114
  is_content_author = True
115
  break
116
+ # 任务发布者
117
+ if not is_content_author:
118
+ for task in tasks_db:
119
+ if task["id"] == item_id and task.get("publisher") == account:
120
+ is_content_author = True
121
+ break
122
  # 检查是否为个人主页留言板作者
123
  if not is_content_author:
124
  if item_id in users_db and item_id == account:
router_items.py CHANGED
@@ -11,6 +11,7 @@ import 数据库连接 as db
11
  from models import ItemCreate, ItemUpdate
12
  from 安全认证 import require_auth, check_ownership
13
  from 数据库连接 import invalidate_cache
 
14
 
15
  router = APIRouter()
16
 
@@ -26,6 +27,9 @@ def get_last_6_months():
26
  res.append(f"{y}-{m:02d}")
27
  return res
28
 
 
 
 
29
  @router.get("/api/items")
30
  async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): # 优化:默认限制调大至 50,提升前端列表体验
31
  items_db = db.load_data("items.json", default_data=[])
@@ -37,6 +41,28 @@ async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): #
37
  filtered_items = [item for item in items_db if item.get("type", "").startswith("recommend")]
38
  else:
39
  filtered_items = [item for item in items_db if item.get("type") == type]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  for item in filtered_items:
42
  item["commentsData"] = comments_db.get(item["id"], [])
@@ -46,14 +72,7 @@ async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): #
46
  # 🔴 【绝对核心防线】:在下发给前端前,强行在内存中抹除敏感信息!
47
  item.pop("github_token", None)
48
  item.pop("netdisk_password", None) # ☁️ 网盘密码不在列表中显示
49
-
50
- if sort == "likes": filtered_items.sort(key=lambda x: x.get("likes", 0), reverse=True)
51
- elif sort == "favorites": filtered_items.sort(key=lambda x: x.get("favorites", 0), reverse=True)
52
- elif sort == "downloads": filtered_items.sort(key=lambda x: x.get("uses", 0), reverse=True)
53
- elif sort == "tips": # 🚀 新增:按近期打赏排序
54
- current_month = datetime.date.today().strftime("%Y-%m")
55
- filtered_items.sort(key=lambda x: x.get("tip_history", {}).get(current_month, 0), reverse=True)
56
- else: filtered_items.sort(key=lambda x: x.get("created_at", 0), reverse=True)
57
 
58
  return {"status": "success", "data": filtered_items[:limit]}
59
 
@@ -141,10 +160,13 @@ async def create_item(item: ItemCreate):
141
  "github_token": item.github_token,
142
  "netdisk_password": item.netdisk_password, # ☁️ 网盘密码
143
  "is_netdisk": item.is_netdisk, # ☁️ 是否网盘资源
 
144
  "likes": 0, "favorites": 0, "comments": 0, "uses": 0, "use_history": {}, "created_at": int(time.time()), "liked_by": [], "favorited_by": []
145
  }
146
  items_db.insert(0, new_item)
147
  db.save_data("items.json", items_db)
 
 
148
  return {"status": "success", "data": new_item}
149
 
150
  @router.put("/api/items/{item_id}")
@@ -200,8 +222,11 @@ async def update_item(item_id: str, update_data: ItemUpdate, current_user: str =
200
  if update_data.github_token is not None: item["github_token"] = update_data.github_token
201
  if update_data.netdisk_password is not None: item["netdisk_password"] = update_data.netdisk_password # ☁️
202
  if update_data.is_netdisk is not None: item["is_netdisk"] = update_data.is_netdisk # ☁️
 
203
 
204
  db.save_data("items.json", items_db)
 
 
205
 
206
  result = {"status": "success"}
207
  if price_change_info:
@@ -313,7 +338,97 @@ async def delete_item(item_id: str, current_user: str = Depends(require_auth)):
313
  # 3. 清理缓存:使 items.json 和 comments.json 的缓存失效
314
  invalidate_cache("items.json")
315
  invalidate_cache("comments.json")
 
 
316
 
317
  return {"status": "success", "message": "内容已删除"}
318
 
319
- raise HTTPException(status_code=404, detail="找不到该内容记录")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  from models import ItemCreate, ItemUpdate
12
  from 安全认证 import require_auth, check_ownership
13
  from 数据库连接 import invalidate_cache
14
+ from db_utils import record_view, sort_cache
15
 
16
  router = APIRouter()
17
 
 
27
  res.append(f"{y}-{m:02d}")
28
  return res
29
 
30
+ # 数据文件标识,用于排序缓存
31
+ data_file = "items.json"
32
+
33
  @router.get("/api/items")
34
  async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): # 优化:默认限制调大至 50,提升前端列表体验
35
  items_db = db.load_data("items.json", default_data=[])
 
41
  filtered_items = [item for item in items_db if item.get("type", "").startswith("recommend")]
42
  else:
43
  filtered_items = [item for item in items_db if item.get("type") == type]
44
+
45
+ # 🗂️ 使用排序缓存优化排序性能
46
+ cache_key = f"items:{type}:{sort}"
47
+
48
+ def sort_fn(data):
49
+ if sort == "likes":
50
+ data.sort(key=lambda x: x.get("likes", 0), reverse=True)
51
+ elif sort == "favorites":
52
+ data.sort(key=lambda x: x.get("favorites", 0), reverse=True)
53
+ elif sort == "downloads":
54
+ data.sort(key=lambda x: x.get("uses", 0), reverse=True)
55
+ elif sort == "tips": # 🚀 按近期打赏排序
56
+ current_month = datetime.date.today().strftime("%Y-%m")
57
+ data.sort(key=lambda x: x.get("tip_history", {}).get(current_month, 0), reverse=True)
58
+ elif sort == "views": # 👁️ 按总访问量排序
59
+ data.sort(key=lambda x: x.get("views", 0), reverse=True)
60
+ elif sort == "daily_views": # 👁️ 按日访问量排序
61
+ data.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
62
+ else: # time 或其他默认
63
+ data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
64
+
65
+ filtered_items = sort_cache.get_sorted(cache_key, filtered_items, sort_fn)
66
 
67
  for item in filtered_items:
68
  item["commentsData"] = comments_db.get(item["id"], [])
 
72
  # 🔴 【绝对核心防线】:在下发给前端前,强行在内存中抹除敏感信息!
73
  item.pop("github_token", None)
74
  item.pop("netdisk_password", None) # ☁️ 网盘密码不在列表中显示
75
+ item.pop("viewed_by", None) # 👁️ 访问者列表不暴露给前端
 
 
 
 
 
 
 
76
 
77
  return {"status": "success", "data": filtered_items[:limit]}
78
 
 
160
  "github_token": item.github_token,
161
  "netdisk_password": item.netdisk_password, # ☁️ 网盘密码
162
  "is_netdisk": item.is_netdisk, # ☁️ 是否网盘资源
163
+ "is_original": item.is_original, # 🎨 是否为原创作品
164
  "likes": 0, "favorites": 0, "comments": 0, "uses": 0, "use_history": {}, "created_at": int(time.time()), "liked_by": [], "favorited_by": []
165
  }
166
  items_db.insert(0, new_item)
167
  db.save_data("items.json", items_db)
168
+ # 🗂️ 清除排序缓存
169
+ sort_cache.invalidate("items:")
170
  return {"status": "success", "data": new_item}
171
 
172
  @router.put("/api/items/{item_id}")
 
222
  if update_data.github_token is not None: item["github_token"] = update_data.github_token
223
  if update_data.netdisk_password is not None: item["netdisk_password"] = update_data.netdisk_password # ☁️
224
  if update_data.is_netdisk is not None: item["is_netdisk"] = update_data.is_netdisk # ☁️
225
+ if update_data.is_original is not None: item["is_original"] = update_data.is_original # 🎨
226
 
227
  db.save_data("items.json", items_db)
228
+ # 🗂️ 清除排序缓存
229
+ sort_cache.invalidate("items:")
230
 
231
  result = {"status": "success"}
232
  if price_change_info:
 
338
  # 3. 清理缓存:使 items.json 和 comments.json 的缓存失效
339
  invalidate_cache("items.json")
340
  invalidate_cache("comments.json")
341
+ # 🗂️ 清除排序缓存
342
+ sort_cache.invalidate("items:")
343
 
344
  return {"status": "success", "message": "内容已删除"}
345
 
346
+ raise HTTPException(status_code=404, detail="找不到该内容记录")
347
+
348
+
349
+ @router.post("/api/items/{item_id}/view")
350
+ async def record_item_view(item_id: str, current_user: str = Depends(require_auth)):
351
+ """
352
+ 记录资源访问量
353
+ 👁️ 需要用户认证,每个用户只计算一次总访问量,日访问量每次调用都增加
354
+ """
355
+ result = record_view("items.json", item_id, current_user)
356
+
357
+ if result is None:
358
+ raise HTTPException(status_code=404, detail="找不到该内容记录")
359
+
360
+ return {"status": "success", "views": result["views"], "daily_views": result["daily_views"]}
361
+
362
+
363
+ # ==========================================
364
+ # ❤️ 互动接口(点赞/收藏)
365
+ # ==========================================
366
+
367
+ @router.post("/api/items/{item_id}/like")
368
+ async def toggle_item_like(item_id: str, current_user: str = Depends(require_auth)):
369
+ """
370
+ 点赞/取消点赞(原子操作,并发安全)
371
+ """
372
+ result_container = [None]
373
+
374
+ def updater(data):
375
+ for item in data:
376
+ if item["id"] == item_id:
377
+ liked_by = item.get("liked_by", [])
378
+ if current_user in liked_by:
379
+ liked_by.remove(current_user)
380
+ item["likes"] = max(0, item.get("likes", 0) - 1)
381
+ action = "unliked"
382
+ else:
383
+ liked_by.append(current_user)
384
+ item["likes"] = item.get("likes", 0) + 1
385
+ action = "liked"
386
+ item["liked_by"] = liked_by
387
+ result_container[0] = {"status": "success", "action": action, "likes": item["likes"]}
388
+ return
389
+ result_container[0] = None # 未找到资源
390
+
391
+ db.atomic_update("items.json", updater, default_data=[])
392
+
393
+ if result_container[0] is None:
394
+ raise HTTPException(status_code=404, detail="资源不存在")
395
+
396
+ # 🗂️ 清除排序缓存(点赞数变化可能影响排序)
397
+ sort_cache.invalidate("items:")
398
+
399
+ return result_container[0]
400
+
401
+
402
+ @router.post("/api/items/{item_id}/favorite")
403
+ async def toggle_item_favorite(item_id: str, current_user: str = Depends(require_auth)):
404
+ """
405
+ 收藏/取消收藏(原子操作,并发安全)
406
+ """
407
+ result_container = [None]
408
+
409
+ def updater(data):
410
+ for item in data:
411
+ if item["id"] == item_id:
412
+ favorited_by = item.get("favorited_by", [])
413
+ if current_user in favorited_by:
414
+ favorited_by.remove(current_user)
415
+ item["favorites"] = max(0, item.get("favorites", 0) - 1)
416
+ action = "unfavorited"
417
+ else:
418
+ favorited_by.append(current_user)
419
+ item["favorites"] = item.get("favorites", 0) + 1
420
+ action = "favorited"
421
+ item["favorited_by"] = favorited_by
422
+ result_container[0] = {"status": "success", "action": action, "favorites": item["favorites"]}
423
+ return
424
+ result_container[0] = None # 未找到资源
425
+
426
+ db.atomic_update("items.json", updater, default_data=[])
427
+
428
+ if result_container[0] is None:
429
+ raise HTTPException(status_code=404, detail="资源不存在")
430
+
431
+ # 🗂️ 清除排序缓存(收藏数变化可能影响排序)
432
+ sort_cache.invalidate("items:")
433
+
434
+ return result_container[0]
router_messages.py CHANGED
@@ -8,24 +8,10 @@ import os
8
  import 数据库连接 as db
9
  from notifications import add_notification
10
  from models import PrivateMessage
11
- from 安全认证 import require_auth
12
 
13
  router = APIRouter()
14
 
15
- # ==========================================
16
- # 🔒 P0安全修复:管理员账号从环境变量读取
17
- # ==========================================
18
- # 支持多管理员,逗号分隔,如:ADMIN_ACCOUNTS=admin1,admin2,admin3
19
- ADMIN_ACCOUNTS = set(
20
- acc.strip()
21
- for acc in os.environ.get("ADMIN_ACCOUNTS", "").split(",")
22
- if acc.strip()
23
- )
24
-
25
- def is_admin(account: str) -> bool:
26
- """检查账号是否为管理员"""
27
- return account in ADMIN_ACCOUNTS
28
-
29
  # ==========================================
30
  # 新增:系统公告请求体模型
31
  # ==========================================
 
8
  import 数据库连接 as db
9
  from notifications import add_notification
10
  from models import PrivateMessage
11
+ from 安全认证 import require_auth, is_admin
12
 
13
  router = APIRouter()
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # ==========================================
16
  # 新增:系统公告请求体模型
17
  # ==========================================
router_posts.py CHANGED
@@ -9,6 +9,7 @@ from fastapi import APIRouter, HTTPException, Depends
9
  from models import PostCreate, PostUpdate
10
  import 数据库连接 as db
11
  from 安全认证 import require_auth
 
12
  import time
13
  import uuid
14
 
@@ -19,9 +20,14 @@ router = APIRouter()
19
  # ==========================================
20
 
21
  @router.get("/api/posts")
22
- async def get_posts(page: int = 1, limit: int = 20):
23
  """
24
- 获取帖子列表(分页,按时间倒序)
 
 
 
 
 
25
  """
26
  posts_db = db.load_data("posts.json", default_data=[])
27
  users_db = db.load_data("users.json", default_data={})
@@ -29,15 +35,29 @@ async def get_posts(page: int = 1, limit: int = 20):
29
  # users_db 已经是 {account: user_info} 格式,直接使用
30
  user_map = users_db
31
 
32
- # 按创建时间倒
33
- sorted_posts = sorted(posts_db, key=lambda x: x.get("created_at", 0), reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  # 分页
36
  start = (page - 1) * limit
37
  end = start + limit
38
  paged_posts = sorted_posts[start:end]
39
 
40
- # 附加作者信息
41
  result = []
42
  for post in paged_posts:
43
  author_info = user_map.get(post.get("author"), {})
@@ -46,6 +66,10 @@ async def get_posts(page: int = 1, limit: int = 20):
46
  "author_name": author_info.get("name", post.get("author")),
47
  "author_avatar": author_info.get("avatarDataUrl", "")
48
  }
 
 
 
 
49
  result.append(post_data)
50
 
51
  return {
@@ -69,9 +93,18 @@ async def get_my_posts(current_user: str = Depends(require_auth)):
69
  # 按创建时间倒序
70
  my_posts = sorted(my_posts, key=lambda x: x.get("created_at", 0), reverse=True)
71
 
 
 
 
 
 
 
 
 
 
72
  return {
73
  "status": "success",
74
- "data": my_posts
75
  }
76
 
77
  @router.get("/api/posts/{post_id}")
@@ -88,13 +121,16 @@ async def get_post_detail(post_id: str):
88
  for post in posts_db:
89
  if post["id"] == post_id:
90
  author_info = user_map.get(post.get("author"), {})
 
 
 
 
 
 
 
91
  return {
92
  "status": "success",
93
- "data": {
94
- **post,
95
- "author_name": author_info.get("name", post.get("author")),
96
- "author_avatar": author_info.get("avatarDataUrl", "")
97
- }
98
  }
99
 
100
  raise HTTPException(status_code=404, detail="帖子不存在")
@@ -117,6 +153,7 @@ async def create_post(post: PostCreate, current_user: str = Depends(require_auth
117
  "images": images,
118
  "author": current_user,
119
  "created_at": int(time.time()),
 
120
  # 互动数据
121
  "likes": 0,
122
  "favorites": 0,
@@ -128,6 +165,8 @@ async def create_post(post: PostCreate, current_user: str = Depends(require_auth
128
 
129
  posts_db.insert(0, new_post)
130
  db.save_data("posts.json", posts_db)
 
 
131
 
132
  return {"status": "success", "data": new_post}
133
 
@@ -151,8 +190,12 @@ async def update_post(post_id: str, update_data: PostUpdate, current_user: str =
151
  post["cover_image"] = update_data.cover_image
152
  if update_data.images is not None:
153
  post["images"] = update_data.images[:9]
 
 
154
 
155
  db.save_data("posts.json", posts_db)
 
 
156
  return {"status": "success"}
157
 
158
  raise HTTPException(status_code=404, detail="帖子不存在")
@@ -171,6 +214,8 @@ async def delete_post(post_id: str, current_user: str = Depends(require_auth)):
171
 
172
  posts_db.pop(i)
173
  db.save_data("posts.json", posts_db)
 
 
174
  return {"status": "success"}
175
 
176
  raise HTTPException(status_code=404, detail="帖子不存在")
@@ -182,56 +227,70 @@ async def delete_post(post_id: str, current_user: str = Depends(require_auth)):
182
  @router.post("/api/posts/{post_id}/like")
183
  async def toggle_like(post_id: str, current_user: str = Depends(require_auth)):
184
  """
185
- 点赞/取消点赞
186
  """
187
- posts_db = db.load_data("posts.json", default_data=[])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
- for post in posts_db:
190
- if post["id"] == post_id:
191
- liked_by = post.get("liked_by", [])
192
-
193
- if current_user in liked_by:
194
- liked_by.remove(current_user)
195
- post["likes"] = max(0, post.get("likes", 0) - 1)
196
- action = "unliked"
197
- else:
198
- liked_by.append(current_user)
199
- post["likes"] = post.get("likes", 0) + 1
200
- action = "liked"
201
-
202
- post["liked_by"] = liked_by
203
- db.save_data("posts.json", posts_db)
204
-
205
- return {"status": "success", "action": action, "likes": post["likes"]}
206
 
207
- raise HTTPException(status_code=404, detail="帖子不存在")
208
 
209
  @router.post("/api/posts/{post_id}/favorite")
210
  async def toggle_favorite(post_id: str, current_user: str = Depends(require_auth)):
211
  """
212
- 收藏/取消收藏
213
  """
214
- posts_db = db.load_data("posts.json", default_data=[])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- for post in posts_db:
217
- if post["id"] == post_id:
218
- favorited_by = post.get("favorited_by", [])
219
-
220
- if current_user in favorited_by:
221
- favorited_by.remove(current_user)
222
- post["favorites"] = max(0, post.get("favorites", 0) - 1)
223
- action = "unfavorited"
224
- else:
225
- favorited_by.append(current_user)
226
- post["favorites"] = post.get("favorites", 0) + 1
227
- action = "favorited"
228
-
229
- post["favorited_by"] = favorited_by
230
- db.save_data("posts.json", posts_db)
231
-
232
- return {"status": "success", "action": action, "favorites": post["favorites"]}
233
 
234
- raise HTTPException(status_code=404, detail="帖子不存在")
235
 
236
  # ==========================================
237
  # 🎁 打赏接口
@@ -240,62 +299,78 @@ async def toggle_favorite(post_id: str, current_user: str = Depends(require_auth
240
  @router.post("/api/posts/{post_id}/tip")
241
  async def tip_post(post_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth)):
242
  """
243
- 打赏帖子
244
  """
245
  if amount <= 0:
246
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
247
 
248
- posts_db = db.load_data("posts.json", default_data=[])
249
- users_db = db.load_data("users.json", default_data={})
250
-
251
- # 查找帖子
252
- target_post = None
253
- for post in posts_db:
254
- if post["id"] == post_id:
255
- target_post = post
256
- break
257
-
258
- if not target_post:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  raise HTTPException(status_code=404, detail="帖子不存在")
260
-
261
- # 不能打赏自己
262
- if target_post.get("author") == current_user:
263
  raise HTTPException(status_code=400, detail="不能打赏自己的帖子")
264
-
265
- # 检查余额(users_db 是字典格式)
266
- tipper = users_db.get(current_user)
267
-
268
- if not tipper or tipper.get("balance", 0) < amount:
269
  raise HTTPException(status_code=400, detail="余额不足")
 
 
270
 
271
- # 扣款
272
- tipper["balance"] = tipper.get("balance", 0) - amount
273
-
274
- # 给作者加钱
275
- author = users_db.get(target_post.get("author"))
276
- if author:
277
- author["balance"] = author.get("balance", 0) + amount
278
-
279
- # 更新打赏榜单
280
- tip_board = target_post.get("tip_board", [])
281
- existing = next((t for t in tip_board if t["account"] == current_user), None)
282
- if existing:
283
- existing["amount"] += amount
284
- else:
285
- tip_board.append({
286
- "account": current_user,
287
- "amount": amount,
288
- "is_anon": is_anon
289
- })
290
-
291
- # 按金额排序
292
- tip_board.sort(key=lambda x: x["amount"], reverse=True)
293
- target_post["tip_board"] = tip_board
294
-
295
- db.save_data("posts.json", posts_db)
296
- db.save_data("users.json", users_db)
297
-
298
- return {"status": "success", "message": f"成功打赏 {amount} 积分"}
299
 
300
  # ==========================================
301
  # 💬 评论接口(复用通用评论系统)
@@ -356,6 +431,9 @@ async def add_post_comment(post_id: str, content: str, current_user: str = Depen
356
  comments_db[post_id] = post_comments
357
  db.save_data("comments.json", comments_db)
358
 
 
 
 
359
  # 更新帖子评论数
360
  for post in posts_db:
361
  if post["id"] == post_id:
@@ -364,3 +442,17 @@ async def add_post_comment(post_id: str, content: str, current_user: str = Depen
364
  db.save_data("posts.json", posts_db)
365
 
366
  return {"status": "success", "data": new_comment}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from models import PostCreate, PostUpdate
10
  import 数据库连接 as db
11
  from 安全认证 import require_auth
12
+ from db_utils import record_view, sort_cache
13
  import time
14
  import uuid
15
 
 
20
  # ==========================================
21
 
22
  @router.get("/api/posts")
23
+ async def get_posts(page: int = 1, limit: int = 20, sort: str = "latest"):
24
  """
25
+ 获取帖子列表(分页,支持多种排方式
26
+ - sort=latest: 按创建时间降序(默认)
27
+ - sort=likes: 按点赞数降序
28
+ - sort=favorites: 按收藏数降序
29
+ - sort=views: 按总访问量降序
30
+ - sort=daily_views: 按日访问量降序
31
  """
32
  posts_db = db.load_data("posts.json", default_data=[])
33
  users_db = db.load_data("users.json", default_data={})
 
35
  # users_db 已经是 {account: user_info} 格式,直接使用
36
  user_map = users_db
37
 
38
+ # 🗂️ 使用排缓存优化排序性能
39
+ cache_key = f"posts:{sort}"
40
+
41
+ def sort_fn(data):
42
+ if sort == "likes":
43
+ data.sort(key=lambda x: x.get("likes", 0), reverse=True)
44
+ elif sort == "favorites":
45
+ data.sort(key=lambda x: x.get("favorites", 0), reverse=True)
46
+ elif sort == "views":
47
+ data.sort(key=lambda x: x.get("views", 0), reverse=True)
48
+ elif sort == "daily_views":
49
+ data.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
50
+ else: # latest 或其他默认
51
+ data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
52
+
53
+ sorted_posts = sort_cache.get_sorted(cache_key, posts_db, sort_fn)
54
 
55
  # 分页
56
  start = (page - 1) * limit
57
  end = start + limit
58
  paged_posts = sorted_posts[start:end]
59
 
60
+ # 附加作者信息,并过滤敏感字段
61
  result = []
62
  for post in paged_posts:
63
  author_info = user_map.get(post.get("author"), {})
 
66
  "author_name": author_info.get("name", post.get("author")),
67
  "author_avatar": author_info.get("avatarDataUrl", "")
68
  }
69
+ # 过滤敏感字段(列表接口过滤 viewed_by、liked_by、favorited_by)
70
+ post_data.pop("viewed_by", None)
71
+ post_data.pop("liked_by", None)
72
+ post_data.pop("favorited_by", None)
73
  result.append(post_data)
74
 
75
  return {
 
93
  # 按创建时间倒序
94
  my_posts = sorted(my_posts, key=lambda x: x.get("created_at", 0), reverse=True)
95
 
96
+ # 过滤敏感字段(列表接口过滤 viewed_by、liked_by、favorited_by)
97
+ result = []
98
+ for post in my_posts:
99
+ post_data = dict(post)
100
+ post_data.pop("viewed_by", None)
101
+ post_data.pop("liked_by", None)
102
+ post_data.pop("favorited_by", None)
103
+ result.append(post_data)
104
+
105
  return {
106
  "status": "success",
107
+ "data": result
108
  }
109
 
110
  @router.get("/api/posts/{post_id}")
 
121
  for post in posts_db:
122
  if post["id"] == post_id:
123
  author_info = user_map.get(post.get("author"), {})
124
+ post_data = {
125
+ **post,
126
+ "author_name": author_info.get("name", post.get("author")),
127
+ "author_avatar": author_info.get("avatarDataUrl", "")
128
+ }
129
+ # 过滤敏感字段
130
+ post_data.pop("viewed_by", None)
131
  return {
132
  "status": "success",
133
+ "data": post_data
 
 
 
 
134
  }
135
 
136
  raise HTTPException(status_code=404, detail="帖子不存在")
 
153
  "images": images,
154
  "author": current_user,
155
  "created_at": int(time.time()),
156
+ "is_original": post.is_original if post.is_original is not None else False, # 🎨 原创作品标记
157
  # 互动数据
158
  "likes": 0,
159
  "favorites": 0,
 
165
 
166
  posts_db.insert(0, new_post)
167
  db.save_data("posts.json", posts_db)
168
+ # 🗂️ 清除排序缓存
169
+ sort_cache.invalidate("posts:")
170
 
171
  return {"status": "success", "data": new_post}
172
 
 
190
  post["cover_image"] = update_data.cover_image
191
  if update_data.images is not None:
192
  post["images"] = update_data.images[:9]
193
+ if update_data.is_original is not None:
194
+ post["is_original"] = update_data.is_original # 🎨 更新原创作品标记
195
 
196
  db.save_data("posts.json", posts_db)
197
+ # 🗂️ 清除排序缓存
198
+ sort_cache.invalidate("posts:")
199
  return {"status": "success"}
200
 
201
  raise HTTPException(status_code=404, detail="帖子不存在")
 
214
 
215
  posts_db.pop(i)
216
  db.save_data("posts.json", posts_db)
217
+ # 🗂️ 清除排序缓存
218
+ sort_cache.invalidate("posts:")
219
  return {"status": "success"}
220
 
221
  raise HTTPException(status_code=404, detail="帖子不存在")
 
227
  @router.post("/api/posts/{post_id}/like")
228
  async def toggle_like(post_id: str, current_user: str = Depends(require_auth)):
229
  """
230
+ 点赞/取消点赞(原子操作,并发安全)
231
  """
232
+ result_container = [None]
233
+
234
+ def updater(data):
235
+ for post in data:
236
+ if post["id"] == post_id:
237
+ liked_by = post.get("liked_by", [])
238
+ if current_user in liked_by:
239
+ liked_by.remove(current_user)
240
+ post["likes"] = max(0, post.get("likes", 0) - 1)
241
+ action = "unliked"
242
+ else:
243
+ liked_by.append(current_user)
244
+ post["likes"] = post.get("likes", 0) + 1
245
+ action = "liked"
246
+ post["liked_by"] = liked_by
247
+ result_container[0] = {"status": "success", "action": action, "likes": post["likes"]}
248
+ return
249
+ result_container[0] = None # 未找到帖子
250
+
251
+ db.atomic_update("posts.json", updater, default_data=[])
252
+
253
+ if result_container[0] is None:
254
+ raise HTTPException(status_code=404, detail="帖子不存在")
255
 
256
+ # 🗂️ 清除排序缓存(点赞数变化可能影响排序)
257
+ sort_cache.invalidate("posts:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ return result_container[0]
260
 
261
  @router.post("/api/posts/{post_id}/favorite")
262
  async def toggle_favorite(post_id: str, current_user: str = Depends(require_auth)):
263
  """
264
+ 收藏/取消收藏(原子操作,并发安全)
265
  """
266
+ result_container = [None]
267
+
268
+ def updater(data):
269
+ for post in data:
270
+ if post["id"] == post_id:
271
+ favorited_by = post.get("favorited_by", [])
272
+ if current_user in favorited_by:
273
+ favorited_by.remove(current_user)
274
+ post["favorites"] = max(0, post.get("favorites", 0) - 1)
275
+ action = "unfavorited"
276
+ else:
277
+ favorited_by.append(current_user)
278
+ post["favorites"] = post.get("favorites", 0) + 1
279
+ action = "favorited"
280
+ post["favorited_by"] = favorited_by
281
+ result_container[0] = {"status": "success", "action": action, "favorites": post["favorites"]}
282
+ return
283
+ result_container[0] = None # 未找到帖子
284
+
285
+ db.atomic_update("posts.json", updater, default_data=[])
286
+
287
+ if result_container[0] is None:
288
+ raise HTTPException(status_code=404, detail="帖子不存在")
289
 
290
+ # 🗂️ 清除排序缓存(收藏数变化可能影响排序)
291
+ sort_cache.invalidate("posts:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
+ return result_container[0]
294
 
295
  # ==========================================
296
  # 🎁 打赏接口
 
299
  @router.post("/api/posts/{post_id}/tip")
300
  async def tip_post(post_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth)):
301
  """
302
+ 打赏帖子(原子操作,并发安全)
303
  """
304
  if amount <= 0:
305
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
306
 
307
+ result_container = [None]
308
+
309
+ def updater(data):
310
+ # 在锁内查找帖子
311
+ target_post = None
312
+ for post in data:
313
+ if post["id"] == post_id:
314
+ target_post = post
315
+ break
316
+
317
+ if not target_post:
318
+ result_container[0] = {"error": "not_found"}
319
+ return
320
+
321
+ # 不能打赏自己
322
+ if target_post.get("author") == current_user:
323
+ result_container[0] = {"error": "self_tip"}
324
+ return
325
+
326
+ # 在锁内操作用户余额
327
+ users_db = db.load_data("users.json", default_data={})
328
+ tipper = users_db.get(current_user)
329
+ if not tipper or tipper.get("balance", 0) < amount:
330
+ result_container[0] = {"error": "insufficient_balance"}
331
+ return
332
+
333
+ author_account = target_post.get("author")
334
+ author = users_db.get(author_account)
335
+ if not author:
336
+ result_container[0] = {"error": "author_not_found"}
337
+ return
338
+
339
+ # 扣款加款
340
+ tipper["balance"] -= amount
341
+ author["balance"] += amount
342
+
343
+ # 更新打赏榜单
344
+ tip_board = target_post.get("tip_board", [])
345
+ existing = next((t for t in tip_board if t["account"] == current_user), None)
346
+ if existing:
347
+ existing["amount"] += amount
348
+ else:
349
+ tip_board.append({"account": current_user, "amount": amount, "is_anon": is_anon})
350
+ tip_board.sort(key=lambda x: x["amount"], reverse=True)
351
+ target_post["tip_board"] = tip_board
352
+
353
+ # 保存用户数据
354
+ db.save_data("users.json", users_db)
355
+
356
+ result_container[0] = {"status": "success", "message": f"成功打赏 {amount} 积分"}
357
+
358
+ db.atomic_update("posts.json", updater, default_data=[])
359
+
360
+ # 🗂️ 清除排序缓存(打赏可能影响排序)
361
+ sort_cache.invalidate("posts:")
362
+
363
+ result = result_container[0]
364
+ if result is None or result.get("error") == "not_found":
365
  raise HTTPException(status_code=404, detail="帖子不存在")
366
+ if result.get("error") == "self_tip":
 
 
367
  raise HTTPException(status_code=400, detail="不能打赏自己的帖子")
368
+ if result.get("error") == "insufficient_balance":
 
 
 
 
369
  raise HTTPException(status_code=400, detail="余额不足")
370
+ if result.get("error") == "author_not_found":
371
+ raise HTTPException(status_code=404, detail="作者账户不存在")
372
 
373
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  # ==========================================
376
  # 💬 评论接口(复用通用评论系统)
 
431
  comments_db[post_id] = post_comments
432
  db.save_data("comments.json", comments_db)
433
 
434
+ # 🗂️ 清除排序缓存(评论数变化可能影响排序)
435
+ sort_cache.invalidate("posts:")
436
+
437
  # 更新帖子评论数
438
  for post in posts_db:
439
  if post["id"] == post_id:
 
442
  db.save_data("posts.json", posts_db)
443
 
444
  return {"status": "success", "data": new_comment}
445
+
446
+
447
+ @router.post("/api/posts/{post_id}/view")
448
+ async def record_post_view(post_id: str, current_user: str = Depends(require_auth)):
449
+ """
450
+ 记录帖子访问量
451
+ 👁️ 需要用户认证,每个用户只计算一次总访问量,日访问量每次调用都增加
452
+ """
453
+ result = record_view("posts.json", post_id, current_user)
454
+
455
+ if result is None:
456
+ raise HTTPException(status_code=404, detail="帖子不存在")
457
+
458
+ return {"status": "success", "views": result["views"], "daily_views": result["daily_views"]}
router_tasks.py CHANGED
@@ -19,6 +19,7 @@ from 安全认证 import require_auth
19
  from notifications import add_notification
20
  from database_sql import get_db
21
  from models_sql import Wallet, Transaction
 
22
  import time
23
  import uuid
24
  import hashlib
@@ -235,13 +236,26 @@ async def get_tasks(
235
  # 默认不显示已取消和过期的任务
236
  filtered = [t for t in filtered if t.get("status") not in ["cancelled", "expired"]]
237
 
238
- # 排序
239
- if sort == "price":
240
- filtered = sorted(filtered, key=lambda x: x.get("total_price", 0), reverse=True)
241
- elif sort == "deadline":
242
- filtered = sorted(filtered, key=lambda x: x.get("deadline", "9999"))
243
- else: # latest
244
- filtered = sorted(filtered, key=lambda x: x.get("created_at", 0), reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  # 分页
247
  start = (page - 1) * limit
@@ -252,8 +266,10 @@ async def get_tasks(
252
  result = []
253
  for task in paged:
254
  publisher_info = user_map.get(task.get("publisher"), {})
 
 
255
  result.append({
256
- **task,
257
  "publisher_name": publisher_info.get("name", task.get("publisher")),
258
  "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
259
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
@@ -293,10 +309,13 @@ async def get_task_detail(task_id: str, current_user: str = None):
293
  "avatar": app_user.get("avatarDataUrl", "")
294
  })
295
 
 
 
 
296
  return {
297
  "status": "success",
298
  "data": {
299
- **task,
300
  "publisher_name": publisher_info.get("name", task.get("publisher")),
301
  "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
302
  "assignee_name": assignee_info.get("name") if assignee_info else None,
@@ -370,11 +389,20 @@ async def create_task(task: TaskCreate, current_user: str = Depends(require_auth
370
  # 申诉相关
371
  "dispute_id": None, # 关联的申诉ID
372
  # 💳 支付状态
373
- "frozen_amount": task.totalPrice # 已冻结金额
 
 
 
 
 
 
 
374
  }
375
 
376
  tasks_db.insert(0, new_task)
377
  db.save_data("tasks.json", tasks_db)
 
 
378
 
379
  # 💳 记录冻结交易
380
  create_task_transaction(
@@ -439,6 +467,8 @@ async def update_task(task_id: str, update_data: TaskUpdate, current_user: str =
439
  changes.append("截止日期")
440
 
441
  db.save_data("tasks.json", tasks_db)
 
 
442
 
443
  # 📢 如果有接单人,发送通知
444
  if task.get("assignee") and changes:
@@ -503,7 +533,9 @@ async def cancel_task(task_id: str, current_user: str = Depends(require_auth), d
503
  task["refunded"] = frozen_amount > 0
504
  task["refund_amount"] = frozen_amount
505
  db.save_data("tasks.json", tasks_db)
506
-
 
 
507
  return {"status": "success", "refunded_amount": frozen_amount}
508
 
509
  raise HTTPException(status_code=404, detail="任务不存在")
@@ -541,6 +573,8 @@ async def apply_task(task_id: str, message: str = None, current_user: str = Depe
541
  task["applicants"] = applicants
542
 
543
  db.save_data("tasks.json", tasks_db)
 
 
544
 
545
  # 🔔 通知发布者:有人申请接单
546
  add_notification(task.get("publisher"), {
@@ -575,6 +609,8 @@ async def cancel_apply(task_id: str, current_user: str = Depends(require_auth)):
575
 
576
  task["applicants"] = new_applicants
577
  db.save_data("tasks.json", tasks_db)
 
 
578
  return {"status": "success"}
579
 
580
  raise HTTPException(status_code=404, detail="任务不存在")
@@ -628,6 +664,8 @@ async def assign_task(task_id: str, assignee: str, current_user: str = Depends(r
628
 
629
  db.save_data("tasks.json", tasks_db)
630
  db_session.commit()
 
 
631
 
632
  # 🔔 通知接单者:被指派接单
633
  add_notification(assignee, {
@@ -672,6 +710,8 @@ async def submit_task(task_id: str, deliverables: list, note: str = None, curren
672
  task["status"] = "submitted"
673
 
674
  db.save_data("tasks.json", tasks_db)
 
 
675
 
676
  # 🔔 通知发布者:接单者已提交成果
677
  add_notification(task.get("publisher"), {
@@ -760,6 +800,8 @@ async def accept_task(task_id: str, is_accepted: bool, feedback: str = None, cur
760
  logger.warning(f"TASK_COMPLETE_JSON_SAVE_FAILED | 资金已结算但任务状态未更新,需手动修复: {str(e)}")
761
 
762
  logger.info(f"TASK_COMPLETE | publisher={current_user} | assignee={assignee_account} | task={task_id} | total={total_price}")
 
 
763
  message = f"验收通过,已支付 {total_price} 积分给接单者"
764
 
765
  # 🔔 支付通知:接单者收到款项
@@ -785,6 +827,8 @@ async def accept_task(task_id: str, is_accepted: bool, feedback: str = None, cur
785
  message = "验收不通过,接单者可以修改后重新提交"
786
 
787
  db.save_data("tasks.json", tasks_db)
 
 
788
 
789
  # 🔔 通知接单者:验收未通过
790
  add_notification(assignee_account, {
@@ -863,6 +907,8 @@ async def create_dispute(task_id: str, reason: str, evidence: list = None, curre
863
 
864
  db.save_data("tasks.json", tasks_db)
865
  db.save_data("disputes.json", disputes_db)
 
 
866
 
867
  # 🔔 通知对方:已发起申诉
868
  other_party = task.get("assignee") if is_publisher else task.get("publisher")
@@ -932,6 +978,8 @@ async def respond_dispute(dispute_id: str, response: str, evidence: list = None,
932
  dispute["status"] = "responded"
933
 
934
  db.save_data("disputes.json", disputes_db)
 
 
935
 
936
  # 🔔 通知申诉方:对方已回应
937
  add_notification(dispute.get("initiator"), {
@@ -1102,6 +1150,8 @@ async def resolve_dispute(dispute_id: str, resolution: str, ratio: int = None, n
1102
  db.save_data("disputes.json", disputes_db)
1103
  db.save_data("tasks.json", tasks_db)
1104
  db.save_data("users.json", users_db)
 
 
1105
 
1106
  # 🔔 通知双方:仲裁结果
1107
  resolution_text = {
@@ -1154,4 +1204,247 @@ async def get_my_tasks(role: str = "publisher", current_user: str = Depends(requ
1154
  # 按创建时间倒序
1155
  my_tasks = sorted(my_tasks, key=lambda x: x.get("created_at", 0), reverse=True)
1156
 
1157
- return {"status": "success", "data": my_tasks}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  from notifications import add_notification
20
  from database_sql import get_db
21
  from models_sql import Wallet, Transaction
22
+ from db_utils import record_view, sort_cache
23
  import time
24
  import uuid
25
  import hashlib
 
236
  # 默认不显示已取消和过期的任务
237
  filtered = [t for t in filtered if t.get("status") not in ["cancelled", "expired"]]
238
 
239
+ # 🗂️ 使用排序缓存优化排序性能
240
+ cache_key = f"tasks:{status or 'all'}:{sort}"
241
+
242
+ def sort_fn(data):
243
+ if sort == "price":
244
+ data.sort(key=lambda x: x.get("total_price", 0), reverse=True)
245
+ elif sort == "deadline":
246
+ data.sort(key=lambda x: x.get("deadline", "9999"))
247
+ elif sort == "views": # 👁️ 按总访问量排序
248
+ data.sort(key=lambda x: x.get("views", 0), reverse=True)
249
+ elif sort == "daily_views": # 👁️ 按日访问量排序
250
+ data.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
251
+ elif sort == "likes": # ❤️ 按点赞数排序
252
+ data.sort(key=lambda x: x.get("likes", 0), reverse=True)
253
+ elif sort == "favorites": # ❤️ 按收藏数排序
254
+ data.sort(key=lambda x: x.get("favorites", 0), reverse=True)
255
+ else: # latest
256
+ data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
257
+
258
+ filtered = sort_cache.get_sorted(cache_key, filtered, sort_fn)
259
 
260
  # 分页
261
  start = (page - 1) * limit
 
266
  result = []
267
  for task in paged:
268
  publisher_info = user_map.get(task.get("publisher"), {})
269
+ # 👁️ 过滤敏感字段(列表接口过滤 liked_by, favorited_by, viewed_by)
270
+ task_data = {k: v for k, v in task.items() if k not in ["viewed_by", "liked_by", "favorited_by"]}
271
  result.append({
272
+ **task_data,
273
  "publisher_name": publisher_info.get("name", task.get("publisher")),
274
  "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
275
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
 
309
  "avatar": app_user.get("avatarDataUrl", "")
310
  })
311
 
312
+ # 👁️ 过滤敏感字段(详情接口保留 liked_by 和 favorited_by,过滤 viewed_by)
313
+ task_data = {k: v for k, v in task.items() if k != "viewed_by"}
314
+
315
  return {
316
  "status": "success",
317
  "data": {
318
+ **task_data,
319
  "publisher_name": publisher_info.get("name", task.get("publisher")),
320
  "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
321
  "assignee_name": assignee_info.get("name") if assignee_info else None,
 
389
  # 申诉相关
390
  "dispute_id": None, # 关联的申诉ID
391
  # 💳 支付状态
392
+ "frozen_amount": task.totalPrice, # 已冻结金额
393
+ # 互动数据
394
+ "likes": 0,
395
+ "favorites": 0,
396
+ "comments": 0,
397
+ "liked_by": [],
398
+ "favorited_by": [],
399
+ "tip_board": [] # 打赏榜单
400
  }
401
 
402
  tasks_db.insert(0, new_task)
403
  db.save_data("tasks.json", tasks_db)
404
+ # 🗂️ 清除排序缓存
405
+ sort_cache.invalidate("tasks:")
406
 
407
  # 💳 记录冻结交易
408
  create_task_transaction(
 
467
  changes.append("截止日期")
468
 
469
  db.save_data("tasks.json", tasks_db)
470
+ # 🗂️ 清除排序缓存
471
+ sort_cache.invalidate("tasks:")
472
 
473
  # 📢 如果有接单人,发送通知
474
  if task.get("assignee") and changes:
 
533
  task["refunded"] = frozen_amount > 0
534
  task["refund_amount"] = frozen_amount
535
  db.save_data("tasks.json", tasks_db)
536
+ # 🗂️ 清除排序缓存
537
+ sort_cache.invalidate("tasks:")
538
+
539
  return {"status": "success", "refunded_amount": frozen_amount}
540
 
541
  raise HTTPException(status_code=404, detail="任务不存在")
 
573
  task["applicants"] = applicants
574
 
575
  db.save_data("tasks.json", tasks_db)
576
+ # 🗂️ 清除排序缓存
577
+ sort_cache.invalidate("tasks:")
578
 
579
  # 🔔 通知发布者:有人申请接单
580
  add_notification(task.get("publisher"), {
 
609
 
610
  task["applicants"] = new_applicants
611
  db.save_data("tasks.json", tasks_db)
612
+ # 🗂️ 清除排序缓存
613
+ sort_cache.invalidate("tasks:")
614
  return {"status": "success"}
615
 
616
  raise HTTPException(status_code=404, detail="任务不存在")
 
664
 
665
  db.save_data("tasks.json", tasks_db)
666
  db_session.commit()
667
+ # 🗂️ 清除排序缓存
668
+ sort_cache.invalidate("tasks:")
669
 
670
  # 🔔 通知接单者:被指派接单
671
  add_notification(assignee, {
 
710
  task["status"] = "submitted"
711
 
712
  db.save_data("tasks.json", tasks_db)
713
+ # 🗂️ 清除排序缓存
714
+ sort_cache.invalidate("tasks:")
715
 
716
  # 🔔 通知发布者:接单者已提交成果
717
  add_notification(task.get("publisher"), {
 
800
  logger.warning(f"TASK_COMPLETE_JSON_SAVE_FAILED | 资金已结算但任务状态未更新,需手动修复: {str(e)}")
801
 
802
  logger.info(f"TASK_COMPLETE | publisher={current_user} | assignee={assignee_account} | task={task_id} | total={total_price}")
803
+ # 🗂️ 清除排序缓存(任务状态变化)
804
+ sort_cache.invalidate("tasks:")
805
  message = f"验收通过,已支付 {total_price} 积分给接单者"
806
 
807
  # 🔔 支付通知:接单者收到款项
 
827
  message = "验收不通过,接单者可以修改后重新提交"
828
 
829
  db.save_data("tasks.json", tasks_db)
830
+ # 🗂️ 清除排序缓存(任务状态变化)
831
+ sort_cache.invalidate("tasks:")
832
 
833
  # 🔔 通知接单者:验收未通过
834
  add_notification(assignee_account, {
 
907
 
908
  db.save_data("tasks.json", tasks_db)
909
  db.save_data("disputes.json", disputes_db)
910
+ # 🗂️ 清除排序缓存(任务状态变为争议中)
911
+ sort_cache.invalidate("tasks:")
912
 
913
  # 🔔 通知对方:已发起申诉
914
  other_party = task.get("assignee") if is_publisher else task.get("publisher")
 
978
  dispute["status"] = "responded"
979
 
980
  db.save_data("disputes.json", disputes_db)
981
+ # 🗂️ 清除排序缓存(争议状态变化可能影响排序)
982
+ sort_cache.invalidate("tasks:")
983
 
984
  # 🔔 通知申诉方:对方已回应
985
  add_notification(dispute.get("initiator"), {
 
1150
  db.save_data("disputes.json", disputes_db)
1151
  db.save_data("tasks.json", tasks_db)
1152
  db.save_data("users.json", users_db)
1153
+ # 🗂️ 清除排序缓存(任务状态变为已完成)
1154
+ sort_cache.invalidate("tasks:")
1155
 
1156
  # 🔔 通知双方:仲裁结果
1157
  resolution_text = {
 
1204
  # 按创建时间倒序
1205
  my_tasks = sorted(my_tasks, key=lambda x: x.get("created_at", 0), reverse=True)
1206
 
1207
+ # 过滤敏感字段(列表接口过滤 viewed_by、liked_by、favorited_by)
1208
+ result = []
1209
+ for task in my_tasks:
1210
+ task_data = {k: v for k, v in task.items() if k not in ["viewed_by", "liked_by", "favorited_by"]}
1211
+ result.append(task_data)
1212
+
1213
+ return {"status": "success", "data": result}
1214
+
1215
+
1216
+ @router.post("/api/tasks/{task_id}/view")
1217
+ async def record_task_view(task_id: str, current_user: str = Depends(require_auth)):
1218
+ """
1219
+ 记录任务访问量
1220
+ 👁️ 需要用户认证,每个用户只计算一次总访问量,日访问量每次调用都增加
1221
+ """
1222
+ result = record_view("tasks.json", task_id, current_user)
1223
+
1224
+ if result is None:
1225
+ raise HTTPException(status_code=404, detail="任务不存在")
1226
+
1227
+ return {"status": "success", "views": result["views"], "daily_views": result["daily_views"]}
1228
+
1229
+ # ==========================================
1230
+ # ❤️ 互动接口(点赞/收藏)
1231
+ # ==========================================
1232
+
1233
+ @router.post("/api/tasks/{task_id}/like")
1234
+ async def toggle_like(task_id: str, current_user: str = Depends(require_auth)):
1235
+ """
1236
+ 点赞/取消点赞(原子操作,并发安全)
1237
+ """
1238
+ result_container = [None]
1239
+
1240
+ def updater(data):
1241
+ for task in data:
1242
+ if task["id"] == task_id:
1243
+ liked_by = task.get("liked_by", [])
1244
+ if current_user in liked_by:
1245
+ liked_by.remove(current_user)
1246
+ task["likes"] = max(0, task.get("likes", 0) - 1)
1247
+ action = "unliked"
1248
+ else:
1249
+ liked_by.append(current_user)
1250
+ task["likes"] = task.get("likes", 0) + 1
1251
+ action = "liked"
1252
+ task["liked_by"] = liked_by
1253
+ result_container[0] = {"status": "success", "action": action, "likes": task["likes"]}
1254
+ return
1255
+ result_container[0] = None # 未找到任务
1256
+
1257
+ db.atomic_update("tasks.json", updater, default_data=[])
1258
+
1259
+ if result_container[0] is None:
1260
+ raise HTTPException(status_code=404, detail="任务不存在")
1261
+
1262
+ # 🗂️ 清除排序缓存(点赞数变化可能影响排序)
1263
+ sort_cache.invalidate("tasks:")
1264
+
1265
+ return result_container[0]
1266
+
1267
+ @router.post("/api/tasks/{task_id}/favorite")
1268
+ async def toggle_favorite(task_id: str, current_user: str = Depends(require_auth)):
1269
+ """
1270
+ 收藏/取消收藏(原子操作,并发安全)
1271
+ """
1272
+ result_container = [None]
1273
+
1274
+ def updater(data):
1275
+ for task in data:
1276
+ if task["id"] == task_id:
1277
+ favorited_by = task.get("favorited_by", [])
1278
+ if current_user in favorited_by:
1279
+ favorited_by.remove(current_user)
1280
+ task["favorites"] = max(0, task.get("favorites", 0) - 1)
1281
+ action = "unfavorited"
1282
+ else:
1283
+ favorited_by.append(current_user)
1284
+ task["favorites"] = task.get("favorites", 0) + 1
1285
+ action = "favorited"
1286
+ task["favorited_by"] = favorited_by
1287
+ result_container[0] = {"status": "success", "action": action, "favorites": task["favorites"]}
1288
+ return
1289
+ result_container[0] = None # 未找到任务
1290
+
1291
+ db.atomic_update("tasks.json", updater, default_data=[])
1292
+
1293
+ if result_container[0] is None:
1294
+ raise HTTPException(status_code=404, detail="任务不存在")
1295
+
1296
+ # 🗂️ 清除排序缓存(收藏数变化可能影响排序)
1297
+ sort_cache.invalidate("tasks:")
1298
+
1299
+ return result_container[0]
1300
+
1301
+ # ==========================================
1302
+ # 🎁 打赏接口
1303
+ # ==========================================
1304
+
1305
+ @router.post("/api/tasks/{task_id}/tip")
1306
+ async def tip_task(task_id: str, amount: int, is_anon: bool = False, current_user: str = Depends(require_auth)):
1307
+ """
1308
+ 打赏任务(原子操作,并发安全)
1309
+ """
1310
+ if amount <= 0:
1311
+ raise HTTPException(status_code=400, detail="打赏金额必须大于0")
1312
+
1313
+ result_container = [None]
1314
+
1315
+ def updater(data):
1316
+ # 在锁内查找任务
1317
+ target_task = None
1318
+ for task in data:
1319
+ if task["id"] == task_id:
1320
+ target_task = task
1321
+ break
1322
+
1323
+ if not target_task:
1324
+ result_container[0] = {"error": "not_found"}
1325
+ return
1326
+
1327
+ # 不能打赏自己
1328
+ if target_task.get("publisher") == current_user:
1329
+ result_container[0] = {"error": "self_tip"}
1330
+ return
1331
+
1332
+ # 在锁内操作用户余额
1333
+ users_db = db.load_data("users.json", default_data={})
1334
+ tipper = users_db.get(current_user)
1335
+ if not tipper or tipper.get("balance", 0) < amount:
1336
+ result_container[0] = {"error": "insufficient_balance"}
1337
+ return
1338
+
1339
+ publisher = target_task.get("publisher")
1340
+ author = users_db.get(publisher)
1341
+ if not author:
1342
+ result_container[0] = {"error": "author_not_found"}
1343
+ return
1344
+
1345
+ # 扣款加款
1346
+ tipper["balance"] -= amount
1347
+ author["balance"] += amount
1348
+
1349
+ # 更新打赏榜单
1350
+ tip_board = target_task.get("tip_board", [])
1351
+ existing = next((t for t in tip_board if t["account"] == current_user), None)
1352
+ if existing:
1353
+ existing["amount"] += amount
1354
+ else:
1355
+ tip_board.append({"account": current_user, "amount": amount, "is_anon": is_anon})
1356
+ tip_board.sort(key=lambda x: x["amount"], reverse=True)
1357
+ target_task["tip_board"] = tip_board
1358
+
1359
+ # 保存用户数据
1360
+ db.save_data("users.json", users_db)
1361
+
1362
+ result_container[0] = {"status": "success", "message": f"成功打赏 {amount} 积分"}
1363
+
1364
+ db.atomic_update("tasks.json", updater, default_data=[])
1365
+
1366
+ # 🗂️ 清除排序缓存(打赏可能影响排序)
1367
+ sort_cache.invalidate("tasks:")
1368
+
1369
+ result = result_container[0]
1370
+ if result is None or result.get("error") == "not_found":
1371
+ raise HTTPException(status_code=404, detail="任务不存在")
1372
+ if result.get("error") == "self_tip":
1373
+ raise HTTPException(status_code=400, detail="不能打赏自己的任务")
1374
+ if result.get("error") == "insufficient_balance":
1375
+ raise HTTPException(status_code=400, detail="余额不足")
1376
+ if result.get("error") == "author_not_found":
1377
+ raise HTTPException(status_code=404, detail="作者账户不存在")
1378
+
1379
+ return result
1380
+
1381
+ # ==========================================
1382
+ # 💬 评论接口(复用通用评论系统)
1383
+ # ==========================================
1384
+
1385
+ @router.get("/api/tasks/{task_id}/comments")
1386
+ async def get_task_comments(task_id: str):
1387
+ """
1388
+ 获取任务评论
1389
+ """
1390
+ comments_db = db.load_data("comments.json", default_data={})
1391
+ users_db = db.load_data("users.json", default_data={})
1392
+
1393
+ # users_db 已经是 {account: user_info} 格式,直接使用
1394
+ user_map = users_db
1395
+
1396
+ # comments_db 是 {item_id: [comments]} 格式
1397
+ task_comments = comments_db.get(task_id, [])
1398
+
1399
+ # 附加用户信息
1400
+ result = []
1401
+ for c in task_comments:
1402
+ author_info = user_map.get(c.get("author"), {})
1403
+ result.append({
1404
+ **c,
1405
+ "author_name": author_info.get("name", c.get("author")),
1406
+ "author_avatar": author_info.get("avatarDataUrl", "")
1407
+ })
1408
+
1409
+ return {"status": "success", "data": result}
1410
+
1411
+ @router.post("/api/tasks/{task_id}/comments")
1412
+ async def add_task_comment(task_id: str, content: str, current_user: str = Depends(require_auth)):
1413
+ """
1414
+ 添加任务评论
1415
+ """
1416
+ if not content or not content.strip():
1417
+ raise HTTPException(status_code=400, detail="评论内容不能为空")
1418
+
1419
+ tasks_db = db.load_data("tasks.json", default_data=[])
1420
+ comments_db = db.load_data("comments.json", default_data={})
1421
+
1422
+ # 检查任务是否存在
1423
+ task_exists = any(t["id"] == task_id for t in tasks_db)
1424
+ if not task_exists:
1425
+ raise HTTPException(status_code=404, detail="任务不存在")
1426
+
1427
+ new_comment = {
1428
+ "id": f"comment_{int(time.time())}_{uuid.uuid4().hex[:6]}",
1429
+ "author": current_user,
1430
+ "content": content.strip(),
1431
+ "created_at": int(time.time())
1432
+ }
1433
+
1434
+ # comments_db 是 {item_id: [comments]} 格式
1435
+ task_comments = comments_db.get(task_id, [])
1436
+ task_comments.insert(0, new_comment)
1437
+ comments_db[task_id] = task_comments
1438
+ db.save_data("comments.json", comments_db)
1439
+
1440
+ # 🗂️ 清除排序缓存(评论数变化可能影响排序)
1441
+ sort_cache.invalidate("tasks:")
1442
+
1443
+ # 更新任务评论数
1444
+ for task in tasks_db:
1445
+ if task["id"] == task_id:
1446
+ task["comments"] = task.get("comments", 0) + 1
1447
+ break
1448
+ db.save_data("tasks.json", tasks_db)
1449
+
1450
+ return {"status": "success", "data": new_comment}
数据库连接.py CHANGED
@@ -533,3 +533,136 @@ def get_backup_files() -> list:
533
  if not os.path.exists(BACKUP_DIR):
534
  return []
535
  return [f for f in os.listdir(BACKUP_DIR) if f.endswith(".bak")]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  if not os.path.exists(BACKUP_DIR):
534
  return []
535
  return [f for f in os.listdir(BACKUP_DIR) if f.endswith(".bak")]
536
+
537
+
538
+ # ==========================================
539
+ # 🔒 原子更新函数(解决并发安全问题)
540
+ # ==========================================
541
+
542
+ def atomic_update(file_name: str, updater, default_data=None):
543
+ """
544
+ 原子性地读取、修改、保存 JSON 数据文件。
545
+
546
+ 整个过程在同一把文件锁内完成,避免并发覆盖问题。
547
+ 这是解决 read-modify-write 并发安全问题的专用方法。
548
+
549
+ 参数:
550
+ file_name: 文件名(如 items.json)
551
+ updater: 回调函数,接收数据并原地修改,返回任意结果
552
+ default_data: 数据不存在时的默认值
553
+
554
+ 返回:
555
+ updater 的返回值
556
+
557
+ 示例:
558
+ def increment_views(data):
559
+ for item in data:
560
+ if item["id"] == target_id:
561
+ item["views"] = item.get("views", 0) + 1
562
+ return item["views"]
563
+ return None
564
+
565
+ result = atomic_update("items.json", increment_views, default_data=[])
566
+ """
567
+ # 默认值处理
568
+ if default_data is None:
569
+ default_data = {} if file_name == "users.json" else []
570
+
571
+ local_path = os.path.join(LOCAL_DB_DIR, file_name)
572
+ file_lock = _get_file_lock(file_name)
573
+
574
+ with file_lock:
575
+ # ========== 第一步:从文件读取数据(跳过缓存) ==========
576
+ # 在锁内直接读取文件,确保读到最新数据
577
+ if not os.path.exists(local_path):
578
+ # 文件不存在,尝试从 HF 下载
579
+ if HF_TOKEN:
580
+ try:
581
+ downloaded_path = hf_hub_download(
582
+ repo_id=DATASET_REPO_ID,
583
+ repo_type="dataset",
584
+ filename=file_name,
585
+ token=HF_TOKEN
586
+ )
587
+ with open(downloaded_path, "r", encoding="utf-8") as f:
588
+ data = json.load(f)
589
+ # 保存到本地
590
+ with open(local_path, "w", encoding="utf-8") as f:
591
+ json.dump(data, f, ensure_ascii=False, indent=2)
592
+ except Exception as e:
593
+ logger.warning(f"从 HF 下载 {file_name} 失败: {e}")
594
+ data = default_data
595
+ else:
596
+ data = default_data
597
+ else:
598
+ try:
599
+ with open(local_path, "r", encoding="utf-8") as f:
600
+ # 获取共享锁(读锁)
601
+ _lock_file(f, exclusive=False)
602
+ try:
603
+ data = json.load(f)
604
+ finally:
605
+ _unlock_file(f)
606
+ except json.JSONDecodeError as e:
607
+ logger.error(f"JSON 解析错误 {file_name}: {e}")
608
+ # 尝试从备份恢复
609
+ data = _recover_from_backup(file_name, default_data)
610
+ except Exception as e:
611
+ logger.warning(f"读取 {file_name} 失败: {e}")
612
+ data = default_data
613
+
614
+ # ========== 第二步:在锁内执行更新操作 ==========
615
+ result = updater(data)
616
+
617
+ # ========== 第三步:原子写入 ==========
618
+ backup_path = os.path.join(BACKUP_DIR, f"{file_name}.bak")
619
+
620
+ # 备份现有文件
621
+ if os.path.exists(local_path):
622
+ try:
623
+ shutil.copy2(local_path, backup_path)
624
+ except Exception as e:
625
+ logger.warning(f"备份 {file_name} 失败: {e}")
626
+
627
+ # 原子写入:临时文件 + 重命名
628
+ temp_fd, temp_path = tempfile.mkstemp(
629
+ suffix=".tmp",
630
+ prefix=f"{file_name}_",
631
+ dir=LOCAL_DB_DIR
632
+ )
633
+
634
+ try:
635
+ with os.fdopen(temp_fd, "w", encoding="utf-8") as f:
636
+ json.dump(data, f, ensure_ascii=False, indent=2)
637
+
638
+ # 验证写入成功
639
+ with open(temp_path, "r", encoding="utf-8") as f:
640
+ verified_data = json.load(f)
641
+
642
+ if type(verified_data) != type(data):
643
+ raise ValueError("数据类型验证失败")
644
+ if hasattr(data, "__len__") and len(verified_data) != len(data):
645
+ raise ValueError(f"数据长度不一致: {len(verified_data)} vs {len(data)}")
646
+
647
+ # 原子重命名
648
+ if sys.platform == "win32" and os.path.exists(local_path):
649
+ os.remove(local_path)
650
+ os.rename(temp_path, local_path)
651
+
652
+ except Exception as e:
653
+ if os.path.exists(temp_path):
654
+ os.remove(temp_path)
655
+ logger.error(f"保存 {file_name} 失败: {e}")
656
+ raise
657
+
658
+ # ========== 第四步:更新内存缓存 ==========
659
+ _set_to_cache(file_name, data, local_path)
660
+
661
+ # ========== 第五步:异步同步到云端 ==========
662
+ if HF_TOKEN:
663
+ try:
664
+ _upload_executor.submit(_background_upload_to_hf, local_path, file_name)
665
+ except Exception as e:
666
+ logger.warning(f"提交上传任务失败: {e}")
667
+
668
+ return result