Spaces:
Running
Running
增加打分5星机制
Browse files- models.py +12 -1
- router_items.py +82 -2
- router_posts.py +86 -10
models.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# models.py
|
| 2 |
-
from pydantic import BaseModel
|
| 3 |
from typing import Optional, List
|
| 4 |
|
| 5 |
class SendCodeRequest(BaseModel):
|
|
@@ -105,6 +105,17 @@ class InteractionToggle(BaseModel):
|
|
| 105 |
action_type: str
|
| 106 |
is_active: bool
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
class RechargeRequest(BaseModel):
|
| 109 |
account: str
|
| 110 |
amount: int
|
|
|
|
| 1 |
# models.py
|
| 2 |
+
from pydantic import BaseModel, validator
|
| 3 |
from typing import Optional, List
|
| 4 |
|
| 5 |
class SendCodeRequest(BaseModel):
|
|
|
|
| 105 |
action_type: str
|
| 106 |
is_active: bool
|
| 107 |
|
| 108 |
+
class RatingRequest(BaseModel):
|
| 109 |
+
score: int # 1-5
|
| 110 |
+
|
| 111 |
+
@validator("score")
|
| 112 |
+
def validate_score(cls, v):
|
| 113 |
+
if not isinstance(v, int):
|
| 114 |
+
raise ValueError("score must be an integer between 1 and 5")
|
| 115 |
+
if v < 1 or v > 5:
|
| 116 |
+
raise ValueError("score must be between 1 and 5")
|
| 117 |
+
return v
|
| 118 |
+
|
| 119 |
class RechargeRequest(BaseModel):
|
| 120 |
account: str
|
| 121 |
amount: int
|
router_items.py
CHANGED
|
@@ -8,7 +8,7 @@ import urllib.request
|
|
| 8 |
import urllib.error
|
| 9 |
import json
|
| 10 |
import 数据库连接 as db
|
| 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
|
|
@@ -59,6 +59,8 @@ async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): #
|
|
| 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 |
|
|
@@ -293,7 +295,11 @@ async def create_item(item: ItemCreate):
|
|
| 293 |
"views": 0,
|
| 294 |
"daily_views": 0,
|
| 295 |
"viewed_by": [],
|
| 296 |
-
"daily_views_date": ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
items_db.insert(0, new_item)
|
| 299 |
db.save_data("items.json", items_db)
|
|
@@ -598,6 +604,80 @@ async def record_item_use(item_id: str, current_user: str = Depends(require_auth
|
|
| 598 |
# ❤️ 互动接口(点赞/收藏)
|
| 599 |
# ==========================================
|
| 600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
@router.post("/api/items/{item_id}/like")
|
| 602 |
async def toggle_item_like(item_id: str, current_user: str = Depends(require_auth)):
|
| 603 |
"""
|
|
|
|
| 8 |
import urllib.error
|
| 9 |
import json
|
| 10 |
import 数据库连接 as db
|
| 11 |
+
from models import ItemCreate, ItemUpdate, RatingRequest
|
| 12 |
from 安全认证 import require_auth, check_ownership
|
| 13 |
from 数据库连接 import invalidate_cache
|
| 14 |
from db_utils import record_view, sort_cache
|
|
|
|
| 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 |
+
elif sort == "rating": # ⭐ 按评分排序
|
| 63 |
+
data.sort(key=lambda x: (x.get("rating_avg", 0), x.get("rating_count", 0)), reverse=True)
|
| 64 |
else: # time 或其他默认
|
| 65 |
data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
| 66 |
|
|
|
|
| 295 |
"views": 0,
|
| 296 |
"daily_views": 0,
|
| 297 |
"viewed_by": [],
|
| 298 |
+
"daily_views_date": "",
|
| 299 |
+
"rating_avg": 0.0,
|
| 300 |
+
"rating_count": 0,
|
| 301 |
+
"rating_dist": {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0},
|
| 302 |
+
"rated_by": {}
|
| 303 |
}
|
| 304 |
items_db.insert(0, new_item)
|
| 305 |
db.save_data("items.json", items_db)
|
|
|
|
| 604 |
# ❤️ 互动接口(点赞/收藏)
|
| 605 |
# ==========================================
|
| 606 |
|
| 607 |
+
@router.post("/api/items/{item_id}/rating")
|
| 608 |
+
async def rate_item(item_id: str, request: RatingRequest, current_user: str = Depends(require_auth)):
|
| 609 |
+
"""
|
| 610 |
+
为资源评分(原子操作,并发安全)
|
| 611 |
+
⭐ score: 1-5
|
| 612 |
+
"""
|
| 613 |
+
score = request.score
|
| 614 |
+
if score < 1 or score > 5:
|
| 615 |
+
raise HTTPException(status_code=400, detail="评分必须在1-5之间")
|
| 616 |
+
|
| 617 |
+
result_container = [None]
|
| 618 |
+
|
| 619 |
+
def updater(data):
|
| 620 |
+
for item in data:
|
| 621 |
+
if item["id"] == item_id:
|
| 622 |
+
# 禁止自评
|
| 623 |
+
if item.get("author") == current_user:
|
| 624 |
+
result_container[0] = {"error": "self_rating"}
|
| 625 |
+
return
|
| 626 |
+
# 初始化评分字段
|
| 627 |
+
if "rating_avg" not in item:
|
| 628 |
+
item["rating_avg"] = 0.0
|
| 629 |
+
if "rating_count" not in item:
|
| 630 |
+
item["rating_count"] = 0
|
| 631 |
+
if "rating_dist" not in item:
|
| 632 |
+
item["rating_dist"] = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0}
|
| 633 |
+
if "rated_by" not in item:
|
| 634 |
+
item["rated_by"] = {}
|
| 635 |
+
|
| 636 |
+
rated_by = item["rated_by"]
|
| 637 |
+
rating_dist = item["rating_dist"]
|
| 638 |
+
old_score = None
|
| 639 |
+
if current_user in rated_by:
|
| 640 |
+
old_score = rated_by[current_user]["score"]
|
| 641 |
+
|
| 642 |
+
if old_score is not None:
|
| 643 |
+
# 已评分,先减去旧分数分布
|
| 644 |
+
rating_dist[str(old_score)] = max(0, rating_dist.get(str(old_score), 0) - 1)
|
| 645 |
+
rating_dist[str(score)] = rating_dist.get(str(score), 0) + 1
|
| 646 |
+
else:
|
| 647 |
+
# 未评分,增加计数
|
| 648 |
+
item["rating_count"] = item.get("rating_count", 0) + 1
|
| 649 |
+
rating_dist[str(score)] = rating_dist.get(str(score), 0) + 1
|
| 650 |
+
|
| 651 |
+
rated_by[current_user] = {"score": score, "time": int(time.time())}
|
| 652 |
+
|
| 653 |
+
# 重新计算平均分
|
| 654 |
+
total = sum(int(k) * v for k, v in rating_dist.items())
|
| 655 |
+
count = item["rating_count"]
|
| 656 |
+
item["rating_avg"] = round(total / count, 2) if count > 0 else 0.0
|
| 657 |
+
|
| 658 |
+
result_container[0] = {
|
| 659 |
+
"status": "success",
|
| 660 |
+
"rating_avg": item["rating_avg"],
|
| 661 |
+
"rating_count": item["rating_count"],
|
| 662 |
+
"rating_dist": item["rating_dist"],
|
| 663 |
+
"user_score": score
|
| 664 |
+
}
|
| 665 |
+
return
|
| 666 |
+
result_container[0] = None # 未找到资源
|
| 667 |
+
|
| 668 |
+
db.atomic_update("items.json", updater, default_data=[])
|
| 669 |
+
|
| 670 |
+
if result_container[0] is None:
|
| 671 |
+
raise HTTPException(status_code=404, detail="资源不存在")
|
| 672 |
+
if result_container[0].get("error") == "self_rating":
|
| 673 |
+
raise HTTPException(status_code=400, detail="不能给自己发布的资源评分")
|
| 674 |
+
|
| 675 |
+
# 🗂️ 清除排序缓存(评分变化可能影响排序)
|
| 676 |
+
sort_cache.invalidate("items:")
|
| 677 |
+
|
| 678 |
+
return result_container[0]
|
| 679 |
+
|
| 680 |
+
|
| 681 |
@router.post("/api/items/{item_id}/like")
|
| 682 |
async def toggle_item_like(item_id: str, current_user: str = Depends(require_auth)):
|
| 683 |
"""
|
router_posts.py
CHANGED
|
@@ -7,7 +7,7 @@
|
|
| 7 |
|
| 8 |
from fastapi import APIRouter, HTTPException, Depends
|
| 9 |
from sqlalchemy.orm import Session
|
| 10 |
-
from models import PostCreate, PostUpdate
|
| 11 |
import 数据库连接 as db
|
| 12 |
from 安全认证 import require_auth
|
| 13 |
from db_utils import record_view, sort_cache
|
|
@@ -57,6 +57,8 @@ async def get_posts(page: int = 1, limit: int = 20, sort: str = "latest"):
|
|
| 57 |
data.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
|
| 58 |
elif sort == "tips":
|
| 59 |
data.sort(key=lambda x: sum(t.get("amount", 0) for t in x.get("tip_board", [])), reverse=True)
|
|
|
|
|
|
|
| 60 |
else: # latest 或其他默认
|
| 61 |
data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
| 62 |
|
|
@@ -76,12 +78,10 @@ async def get_posts(page: int = 1, limit: int = 20, sort: str = "latest"):
|
|
| 76 |
"author_name": author_info.get("name", post.get("author")),
|
| 77 |
"author_avatar": author_info.get("avatarDataUrl", "")
|
| 78 |
}
|
| 79 |
-
# 过滤敏感字段(列表接口过滤 viewed_by
|
| 80 |
post_data.pop("viewed_by", None)
|
| 81 |
-
post_data.pop("liked_by", None)
|
| 82 |
-
post_data.pop("favorited_by", None)
|
| 83 |
result.append(post_data)
|
| 84 |
-
|
| 85 |
return {
|
| 86 |
"status": "success",
|
| 87 |
"data": result,
|
|
@@ -103,15 +103,13 @@ async def get_my_posts(current_user: str = Depends(require_auth)):
|
|
| 103 |
# 按创建时间倒序
|
| 104 |
my_posts = sorted(my_posts, key=lambda x: x.get("created_at", 0), reverse=True)
|
| 105 |
|
| 106 |
-
# 过滤敏感字段(列表接口过滤 viewed_by
|
| 107 |
result = []
|
| 108 |
for post in my_posts:
|
| 109 |
post_data = dict(post)
|
| 110 |
post_data.pop("viewed_by", None)
|
| 111 |
-
post_data.pop("liked_by", None)
|
| 112 |
-
post_data.pop("favorited_by", None)
|
| 113 |
result.append(post_data)
|
| 114 |
-
|
| 115 |
return {
|
| 116 |
"status": "success",
|
| 117 |
"data": result
|
|
@@ -174,7 +172,11 @@ async def create_post(post: PostCreate, current_user: str = Depends(require_auth
|
|
| 174 |
"views": 0,
|
| 175 |
"daily_views": 0,
|
| 176 |
"viewed_by": [],
|
| 177 |
-
"daily_views_date": ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
posts_db.insert(0, new_post)
|
|
@@ -248,6 +250,80 @@ async def delete_post(post_id: str, current_user: str = Depends(require_auth)):
|
|
| 248 |
# ❤️ 互动接口(点赞/收藏)
|
| 249 |
# ==========================================
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
@router.post("/api/posts/{post_id}/like")
|
| 252 |
async def toggle_like(post_id: str, current_user: str = Depends(require_auth)):
|
| 253 |
"""
|
|
|
|
| 7 |
|
| 8 |
from fastapi import APIRouter, HTTPException, Depends
|
| 9 |
from sqlalchemy.orm import Session
|
| 10 |
+
from models import PostCreate, PostUpdate, RatingRequest
|
| 11 |
import 数据库连接 as db
|
| 12 |
from 安全认证 import require_auth
|
| 13 |
from db_utils import record_view, sort_cache
|
|
|
|
| 57 |
data.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
|
| 58 |
elif sort == "tips":
|
| 59 |
data.sort(key=lambda x: sum(t.get("amount", 0) for t in x.get("tip_board", [])), reverse=True)
|
| 60 |
+
elif sort == "rating": # ⭐ 按评分排序
|
| 61 |
+
data.sort(key=lambda x: (x.get("rating_avg", 0), x.get("rating_count", 0)), reverse=True)
|
| 62 |
else: # latest 或其他默认
|
| 63 |
data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
| 64 |
|
|
|
|
| 78 |
"author_name": author_info.get("name", post.get("author")),
|
| 79 |
"author_avatar": author_info.get("avatarDataUrl", "")
|
| 80 |
}
|
| 81 |
+
# 过滤敏感字段(列表接口仅过滤 viewed_by,保留 liked_by 和 favorited_by)
|
| 82 |
post_data.pop("viewed_by", None)
|
|
|
|
|
|
|
| 83 |
result.append(post_data)
|
| 84 |
+
|
| 85 |
return {
|
| 86 |
"status": "success",
|
| 87 |
"data": result,
|
|
|
|
| 103 |
# 按创建时间倒序
|
| 104 |
my_posts = sorted(my_posts, key=lambda x: x.get("created_at", 0), reverse=True)
|
| 105 |
|
| 106 |
+
# 过滤敏感字段(列表接口仅过滤 viewed_by,保留 liked_by 和 favorited_by)
|
| 107 |
result = []
|
| 108 |
for post in my_posts:
|
| 109 |
post_data = dict(post)
|
| 110 |
post_data.pop("viewed_by", None)
|
|
|
|
|
|
|
| 111 |
result.append(post_data)
|
| 112 |
+
|
| 113 |
return {
|
| 114 |
"status": "success",
|
| 115 |
"data": result
|
|
|
|
| 172 |
"views": 0,
|
| 173 |
"daily_views": 0,
|
| 174 |
"viewed_by": [],
|
| 175 |
+
"daily_views_date": "",
|
| 176 |
+
"rating_avg": 0.0,
|
| 177 |
+
"rating_count": 0,
|
| 178 |
+
"rating_dist": {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0},
|
| 179 |
+
"rated_by": {}
|
| 180 |
}
|
| 181 |
|
| 182 |
posts_db.insert(0, new_post)
|
|
|
|
| 250 |
# ❤️ 互动接口(点赞/收藏)
|
| 251 |
# ==========================================
|
| 252 |
|
| 253 |
+
@router.post("/api/posts/{post_id}/rating")
|
| 254 |
+
async def rate_post(post_id: str, request: RatingRequest, current_user: str = Depends(require_auth)):
|
| 255 |
+
"""
|
| 256 |
+
为帖子评分(原子操作,并发安全)
|
| 257 |
+
⭐ score: 1-5
|
| 258 |
+
"""
|
| 259 |
+
score = request.score
|
| 260 |
+
if score < 1 or score > 5:
|
| 261 |
+
raise HTTPException(status_code=400, detail="评分必须在1-5之间")
|
| 262 |
+
|
| 263 |
+
result_container = [None]
|
| 264 |
+
|
| 265 |
+
def updater(data):
|
| 266 |
+
for post in data:
|
| 267 |
+
if post["id"] == post_id:
|
| 268 |
+
# 禁止自评
|
| 269 |
+
if post.get("author") == current_user:
|
| 270 |
+
result_container[0] = {"error": "self_rating"}
|
| 271 |
+
return
|
| 272 |
+
# 初始化评分字段
|
| 273 |
+
if "rating_avg" not in post:
|
| 274 |
+
post["rating_avg"] = 0.0
|
| 275 |
+
if "rating_count" not in post:
|
| 276 |
+
post["rating_count"] = 0
|
| 277 |
+
if "rating_dist" not in post:
|
| 278 |
+
post["rating_dist"] = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0}
|
| 279 |
+
if "rated_by" not in post:
|
| 280 |
+
post["rated_by"] = {}
|
| 281 |
+
|
| 282 |
+
rated_by = post["rated_by"]
|
| 283 |
+
rating_dist = post["rating_dist"]
|
| 284 |
+
old_score = None
|
| 285 |
+
if current_user in rated_by:
|
| 286 |
+
old_score = rated_by[current_user]["score"]
|
| 287 |
+
|
| 288 |
+
if old_score is not None:
|
| 289 |
+
# 已评分,先减去旧分数分布
|
| 290 |
+
rating_dist[str(old_score)] = max(0, rating_dist.get(str(old_score), 0) - 1)
|
| 291 |
+
rating_dist[str(score)] = rating_dist.get(str(score), 0) + 1
|
| 292 |
+
else:
|
| 293 |
+
# 未评分,增加计数
|
| 294 |
+
post["rating_count"] = post.get("rating_count", 0) + 1
|
| 295 |
+
rating_dist[str(score)] = rating_dist.get(str(score), 0) + 1
|
| 296 |
+
|
| 297 |
+
rated_by[current_user] = {"score": score, "time": int(time.time())}
|
| 298 |
+
|
| 299 |
+
# 重新计算平均��
|
| 300 |
+
total = sum(int(k) * v for k, v in rating_dist.items())
|
| 301 |
+
count = post["rating_count"]
|
| 302 |
+
post["rating_avg"] = round(total / count, 2) if count > 0 else 0.0
|
| 303 |
+
|
| 304 |
+
result_container[0] = {
|
| 305 |
+
"status": "success",
|
| 306 |
+
"rating_avg": post["rating_avg"],
|
| 307 |
+
"rating_count": post["rating_count"],
|
| 308 |
+
"rating_dist": post["rating_dist"],
|
| 309 |
+
"user_score": score
|
| 310 |
+
}
|
| 311 |
+
return
|
| 312 |
+
result_container[0] = None # 未找到帖子
|
| 313 |
+
|
| 314 |
+
db.atomic_update("posts.json", updater, default_data=[])
|
| 315 |
+
|
| 316 |
+
if result_container[0] is None:
|
| 317 |
+
raise HTTPException(status_code=404, detail="帖子不存在")
|
| 318 |
+
if result_container[0].get("error") == "self_rating":
|
| 319 |
+
raise HTTPException(status_code=400, detail="不能给自己发布的帖子评分")
|
| 320 |
+
|
| 321 |
+
# 🗂️ 清除排序缓存(评分变化可能影响排序)
|
| 322 |
+
sort_cache.invalidate("posts:")
|
| 323 |
+
|
| 324 |
+
return result_container[0]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
@router.post("/api/posts/{post_id}/like")
|
| 328 |
async def toggle_like(post_id: str, current_user: str = Depends(require_auth)):
|
| 329 |
"""
|