Spaces:
Running
Running
Upload 5 files
Browse files- app.py +4 -0
- models.py +104 -1
- router_posts.py +278 -0
- router_tasks.py +1411 -0
app.py
CHANGED
|
@@ -31,6 +31,8 @@ from router_users_social import router as users_social_router
|
|
| 31 |
# 其他业务模块
|
| 32 |
# ==========================================
|
| 33 |
from router_items import router as items_router # 📦 内容管理(工具/应用/推荐)
|
|
|
|
|
|
|
| 34 |
from router_comments import router as comments_router # 💬 评论系统
|
| 35 |
from router_messages import router as messages_router # ✉️ 私信系统
|
| 36 |
from router_wallet import router as wallet_router # 💰 钱包/提现
|
|
@@ -76,6 +78,8 @@ app.include_router(users_social_router) # 🤝 关注/隐私
|
|
| 76 |
|
| 77 |
# 其他业务模块
|
| 78 |
app.include_router(items_router) # 📦 内容管理
|
|
|
|
|
|
|
| 79 |
app.include_router(comments_router) # 💬 评论系统
|
| 80 |
app.include_router(messages_router) # ✉️ 私信系统
|
| 81 |
app.include_router(wallet_router) # 💰 钱包/提现
|
|
|
|
| 31 |
# 其他业务模块
|
| 32 |
# ==========================================
|
| 33 |
from router_items import router as items_router # 📦 内容管理(工具/应用/推荐)
|
| 34 |
+
from router_posts import router as posts_router # 💬 讨论区
|
| 35 |
+
from router_tasks import router as tasks_router # 📝 任务榜
|
| 36 |
from router_comments import router as comments_router # 💬 评论系统
|
| 37 |
from router_messages import router as messages_router # ✉️ 私信系统
|
| 38 |
from router_wallet import router as wallet_router # 💰 钱包/提现
|
|
|
|
| 78 |
|
| 79 |
# 其他业务模块
|
| 80 |
app.include_router(items_router) # 📦 内容管理
|
| 81 |
+
app.include_router(posts_router) # 💬 讨论区
|
| 82 |
+
app.include_router(tasks_router) # 📝 任务榜
|
| 83 |
app.include_router(comments_router) # 💬 评论系统
|
| 84 |
app.include_router(messages_router) # ✉️ 私信系统
|
| 85 |
app.include_router(wallet_router) # 💰 钱包/提现
|
models.py
CHANGED
|
@@ -117,4 +117,107 @@ class WithdrawRequest(BaseModel):
|
|
| 117 |
amount: int
|
| 118 |
alipayAccount: str
|
| 119 |
real_name: str
|
| 120 |
-
code: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
amount: int
|
| 118 |
alipayAccount: str
|
| 119 |
real_name: str
|
| 120 |
+
code: str
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# ==========================================
|
| 124 |
+
# 💬 讨论区数据模型
|
| 125 |
+
# ==========================================
|
| 126 |
+
class PostCreate(BaseModel):
|
| 127 |
+
""" 发布讨论区内容 """
|
| 128 |
+
title: str # 标题
|
| 129 |
+
content: str # 文案内容
|
| 130 |
+
coverImage: str # 封面图
|
| 131 |
+
images: Optional[List[str]] = [] # 详情图(最多9张)
|
| 132 |
+
author: str # 作者账号
|
| 133 |
+
|
| 134 |
+
class PostUpdate(BaseModel):
|
| 135 |
+
""" 更新讨论区内容 """
|
| 136 |
+
title: Optional[str] = None
|
| 137 |
+
content: Optional[str] = None
|
| 138 |
+
coverImage: Optional[str] = None
|
| 139 |
+
images: Optional[List[str]] = None
|
| 140 |
+
|
| 141 |
+
class PostInteraction(BaseModel):
|
| 142 |
+
""" 讨论区互动(点赞/收藏) """
|
| 143 |
+
post_id: str
|
| 144 |
+
user_id: str
|
| 145 |
+
action_type: str # like / favorite
|
| 146 |
+
is_active: bool # True=添加, False=取消
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ==========================================
|
| 150 |
+
# 📝 任务榜数据模型
|
| 151 |
+
# ==========================================
|
| 152 |
+
class TaskCreate(BaseModel):
|
| 153 |
+
""" 发布新任务 """
|
| 154 |
+
title: str # 任务标题
|
| 155 |
+
description: str # 任务详情
|
| 156 |
+
referenceImages: Optional[List[str]] = [] # 参考图(最多6张)
|
| 157 |
+
referenceLink: Optional[str] = None # 参考链接
|
| 158 |
+
totalPrice: int # 总价格(积分)
|
| 159 |
+
depositRatio: int # 订金比例(10/20/30/50)
|
| 160 |
+
deadline: str # 截止时间 (ISO格式)
|
| 161 |
+
publisher: str # 发布者账号
|
| 162 |
+
|
| 163 |
+
class TaskUpdate(BaseModel):
|
| 164 |
+
""" 更新任务 """
|
| 165 |
+
title: Optional[str] = None
|
| 166 |
+
description: Optional[str] = None
|
| 167 |
+
referenceImages: Optional[List[str]] = None
|
| 168 |
+
referenceLink: Optional[str] = None
|
| 169 |
+
totalPrice: Optional[int] = None
|
| 170 |
+
depositRatio: Optional[int] = None
|
| 171 |
+
deadline: Optional[str] = None
|
| 172 |
+
|
| 173 |
+
class TaskApply(BaseModel):
|
| 174 |
+
""" 申请接单 """
|
| 175 |
+
task_id: str
|
| 176 |
+
applicant: str # 申请者账号
|
| 177 |
+
message: Optional[str] = None # 申请留言
|
| 178 |
+
|
| 179 |
+
class TaskAssign(BaseModel):
|
| 180 |
+
""" 指派接单者 """
|
| 181 |
+
task_id: str
|
| 182 |
+
publisher: str # 发布者(校验权限)
|
| 183 |
+
assignee: str # 被指派的接单者
|
| 184 |
+
|
| 185 |
+
class TaskSubmit(BaseModel):
|
| 186 |
+
""" 接单者提交成果 """
|
| 187 |
+
task_id: str
|
| 188 |
+
assignee: str # 接单者(校验权限)
|
| 189 |
+
deliverables: List[str] # 交付物图片URL列表
|
| 190 |
+
note: Optional[str] = None # 备注说明
|
| 191 |
+
|
| 192 |
+
class TaskAccept(BaseModel):
|
| 193 |
+
""" 发布者验收 """
|
| 194 |
+
task_id: str
|
| 195 |
+
publisher: str # 发布者(校验权限)
|
| 196 |
+
is_accepted: bool # True=验收通过, False=验收不通过
|
| 197 |
+
feedback: Optional[str] = None # 反馈意见
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# ==========================================
|
| 201 |
+
# ⚖️ P3增强:任务申诉数据模型
|
| 202 |
+
# ==========================================
|
| 203 |
+
class TaskDispute(BaseModel):
|
| 204 |
+
""" 发起申诉 """
|
| 205 |
+
task_id: str # 任务ID
|
| 206 |
+
initiator: str # 申诉发起人(可以是发布者或接单者)
|
| 207 |
+
reason: str # 申诉理由
|
| 208 |
+
evidence: Optional[List[str]] = [] # 证据图片URL列表
|
| 209 |
+
|
| 210 |
+
class TaskDisputeResponse(BaseModel):
|
| 211 |
+
""" 被申诉方回应 """
|
| 212 |
+
dispute_id: str # 申诉ID
|
| 213 |
+
respondent: str # 回应人
|
| 214 |
+
response: str # 回应内容
|
| 215 |
+
evidence: Optional[List[str]] = [] # 证据图片URL列表
|
| 216 |
+
|
| 217 |
+
class TaskDisputeResolve(BaseModel):
|
| 218 |
+
""" 管理员仲裁 """
|
| 219 |
+
dispute_id: str # 申诉ID
|
| 220 |
+
admin_account: str # 管理员账号
|
| 221 |
+
result: str # 仲裁结果: support_initiator / support_respondent / split
|
| 222 |
+
split_ratio: Optional[int] = 50 # 分成比例(申诉方获得的百分比,仅当result=split时有效)
|
| 223 |
+
admin_note: Optional[str] = None # 管理员备注
|
router_posts.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# router_posts.py
|
| 2 |
+
# ==========================================
|
| 3 |
+
# 💬 讨论区路由模块
|
| 4 |
+
# ==========================================
|
| 5 |
+
# 作用:处理讨论区内容的增删改查和互动功能
|
| 6 |
+
# 关联文件:
|
| 7 |
+
# - 数据库连接.py (JSON数据库读写 posts.json)
|
| 8 |
+
# - models.py (PostCreate, PostUpdate, PostInteraction)
|
| 9 |
+
# 前端调用:
|
| 10 |
+
# - 讨论区列表组件.js (获取帖子列表)
|
| 11 |
+
# - 讨论区发布组件.js (发布新帖子)
|
| 12 |
+
# - 讨论区详情组件.js (帖子详情、互动操作)
|
| 13 |
+
# ==========================================
|
| 14 |
+
|
| 15 |
+
from fastapi import APIRouter, HTTPException
|
| 16 |
+
import 数据库连接 as db
|
| 17 |
+
from models import PostCreate, PostUpdate, PostInteraction
|
| 18 |
+
import time
|
| 19 |
+
import uuid
|
| 20 |
+
|
| 21 |
+
# 创建子路由实例
|
| 22 |
+
router = APIRouter()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ==========================================
|
| 26 |
+
# 📖 获取讨论区列表
|
| 27 |
+
# ==========================================
|
| 28 |
+
@router.get("/api/posts")
|
| 29 |
+
async def get_posts(sort: str = "time", page: int = 1, page_size: int = 20):
|
| 30 |
+
"""
|
| 31 |
+
获取讨论区帖子列表
|
| 32 |
+
|
| 33 |
+
查询参数:
|
| 34 |
+
- sort: 排序方式 (time=最新, hot=最热)
|
| 35 |
+
- page: 页码
|
| 36 |
+
- page_size: 每页数量
|
| 37 |
+
"""
|
| 38 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 39 |
+
|
| 40 |
+
# 排序
|
| 41 |
+
if sort == "hot":
|
| 42 |
+
# 最热:按点赞+收藏加权排序
|
| 43 |
+
posts_db.sort(key=lambda x: x.get("likes", 0) * 2 + x.get("favorites", 0), reverse=True)
|
| 44 |
+
else:
|
| 45 |
+
# 最新:按创建时间倒序
|
| 46 |
+
posts_db.sort(key=lambda x: x.get("createdAt", 0), reverse=True)
|
| 47 |
+
|
| 48 |
+
# 分页
|
| 49 |
+
start = (page - 1) * page_size
|
| 50 |
+
end = start + page_size
|
| 51 |
+
paginated = posts_db[start:end]
|
| 52 |
+
|
| 53 |
+
return {
|
| 54 |
+
"status": "success",
|
| 55 |
+
"data": paginated,
|
| 56 |
+
"total": len(posts_db),
|
| 57 |
+
"page": page,
|
| 58 |
+
"page_size": page_size
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ==========================================
|
| 63 |
+
# 📖 获取单个帖子详情
|
| 64 |
+
# ==========================================
|
| 65 |
+
@router.get("/api/posts/{post_id}")
|
| 66 |
+
async def get_post_detail(post_id: str):
|
| 67 |
+
"""
|
| 68 |
+
获取帖子详情
|
| 69 |
+
"""
|
| 70 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 71 |
+
|
| 72 |
+
post = next((p for p in posts_db if p.get("id") == post_id), None)
|
| 73 |
+
if not post:
|
| 74 |
+
raise HTTPException(status_code=404, detail="帖子不存在")
|
| 75 |
+
|
| 76 |
+
# 增加浏览量
|
| 77 |
+
post["views"] = post.get("views", 0) + 1
|
| 78 |
+
db.save_data("posts.json", posts_db)
|
| 79 |
+
|
| 80 |
+
return {"status": "success", "data": post}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ==========================================
|
| 84 |
+
# ✏️ 发布新帖子
|
| 85 |
+
# ==========================================
|
| 86 |
+
@router.post("/api/posts")
|
| 87 |
+
async def create_post(post_data: PostCreate):
|
| 88 |
+
"""
|
| 89 |
+
发布新帖子
|
| 90 |
+
"""
|
| 91 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 92 |
+
users_db = db.load_data("users.json", default_data={})
|
| 93 |
+
|
| 94 |
+
# 获取作者信息
|
| 95 |
+
author_info = users_db.get(post_data.author, {})
|
| 96 |
+
|
| 97 |
+
# 构建新帖子对象
|
| 98 |
+
new_post = {
|
| 99 |
+
"id": f"post_{uuid.uuid4().hex[:12]}",
|
| 100 |
+
"type": "discussion",
|
| 101 |
+
"title": post_data.title,
|
| 102 |
+
"content": post_data.content,
|
| 103 |
+
"coverImage": post_data.coverImage,
|
| 104 |
+
"images": post_data.images or [],
|
| 105 |
+
|
| 106 |
+
# 作者信息快照
|
| 107 |
+
"author": post_data.author,
|
| 108 |
+
"authorName": author_info.get("name", post_data.author),
|
| 109 |
+
"authorAvatar": author_info.get("avatarDataUrl", ""),
|
| 110 |
+
|
| 111 |
+
# 互动数据初始化
|
| 112 |
+
"likes": 0,
|
| 113 |
+
"favorites": 0,
|
| 114 |
+
"comments": 0,
|
| 115 |
+
"views": 0,
|
| 116 |
+
|
| 117 |
+
# 点赞/收藏用户列表(用于判断当前用户是否已操作)
|
| 118 |
+
"likedBy": [],
|
| 119 |
+
"favoritedBy": [],
|
| 120 |
+
|
| 121 |
+
# 打赏数据
|
| 122 |
+
"tip_board": [],
|
| 123 |
+
"totalTips": 0,
|
| 124 |
+
|
| 125 |
+
# 时间戳
|
| 126 |
+
"createdAt": int(time.time()),
|
| 127 |
+
"updatedAt": int(time.time())
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
posts_db.insert(0, new_post) # 插入到列表头部
|
| 131 |
+
db.save_data("posts.json", posts_db)
|
| 132 |
+
|
| 133 |
+
return {"status": "success", "data": new_post}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ==========================================
|
| 137 |
+
# ✏️ 更新帖子
|
| 138 |
+
# ==========================================
|
| 139 |
+
@router.put("/api/posts/{post_id}")
|
| 140 |
+
async def update_post(post_id: str, update_data: PostUpdate, author: str = None):
|
| 141 |
+
"""
|
| 142 |
+
更新帖子内容
|
| 143 |
+
"""
|
| 144 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 145 |
+
|
| 146 |
+
post_idx = next((i for i, p in enumerate(posts_db) if p.get("id") == post_id), None)
|
| 147 |
+
if post_idx is None:
|
| 148 |
+
raise HTTPException(status_code=404, detail="帖子不存在")
|
| 149 |
+
|
| 150 |
+
post = posts_db[post_idx]
|
| 151 |
+
|
| 152 |
+
# 权限校验:只有作者可以修改
|
| 153 |
+
if author and post.get("author") != author:
|
| 154 |
+
raise HTTPException(status_code=403, detail="无权修改此帖子")
|
| 155 |
+
|
| 156 |
+
# 更新字段
|
| 157 |
+
for k, v in update_data.dict(exclude_unset=True).items():
|
| 158 |
+
if v is not None:
|
| 159 |
+
post[k] = v
|
| 160 |
+
|
| 161 |
+
post["updatedAt"] = int(time.time())
|
| 162 |
+
db.save_data("posts.json", posts_db)
|
| 163 |
+
|
| 164 |
+
return {"status": "success", "data": post}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# ==========================================
|
| 168 |
+
# 🗑️ 删除帖子
|
| 169 |
+
# ==========================================
|
| 170 |
+
@router.delete("/api/posts/{post_id}")
|
| 171 |
+
async def delete_post(post_id: str, author: str = None):
|
| 172 |
+
"""
|
| 173 |
+
删除帖子
|
| 174 |
+
"""
|
| 175 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 176 |
+
|
| 177 |
+
post_idx = next((i for i, p in enumerate(posts_db) if p.get("id") == post_id), None)
|
| 178 |
+
if post_idx is None:
|
| 179 |
+
raise HTTPException(status_code=404, detail="帖子不存在")
|
| 180 |
+
|
| 181 |
+
post = posts_db[post_idx]
|
| 182 |
+
|
| 183 |
+
# 权限校验
|
| 184 |
+
if author and post.get("author") != author:
|
| 185 |
+
raise HTTPException(status_code=403, detail="无权删除此帖子")
|
| 186 |
+
|
| 187 |
+
posts_db.pop(post_idx)
|
| 188 |
+
db.save_data("posts.json", posts_db)
|
| 189 |
+
|
| 190 |
+
return {"status": "success", "message": "帖子已删除"}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ==========================================
|
| 194 |
+
# ❤️ 点赞/收藏操作
|
| 195 |
+
# ==========================================
|
| 196 |
+
@router.post("/api/posts/interaction")
|
| 197 |
+
async def toggle_interaction(req: PostInteraction):
|
| 198 |
+
"""
|
| 199 |
+
点赞或收藏帖子
|
| 200 |
+
|
| 201 |
+
请求参数:
|
| 202 |
+
- post_id: 帖子ID
|
| 203 |
+
- user_id: 用户账号
|
| 204 |
+
- action_type: like / favorite
|
| 205 |
+
- is_active: True=添加, False=取消
|
| 206 |
+
"""
|
| 207 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 208 |
+
users_db = db.load_data("users.json", default_data={})
|
| 209 |
+
|
| 210 |
+
post_idx = next((i for i, p in enumerate(posts_db) if p.get("id") == req.post_id), None)
|
| 211 |
+
if post_idx is None:
|
| 212 |
+
raise HTTPException(status_code=404, detail="帖子不存在")
|
| 213 |
+
|
| 214 |
+
post = posts_db[post_idx]
|
| 215 |
+
|
| 216 |
+
if req.action_type == "like":
|
| 217 |
+
liked_by = post.get("likedBy", [])
|
| 218 |
+
if req.is_active and req.user_id not in liked_by:
|
| 219 |
+
liked_by.append(req.user_id)
|
| 220 |
+
post["likes"] = post.get("likes", 0) + 1
|
| 221 |
+
elif not req.is_active and req.user_id in liked_by:
|
| 222 |
+
liked_by.remove(req.user_id)
|
| 223 |
+
post["likes"] = max(0, post.get("likes", 0) - 1)
|
| 224 |
+
post["likedBy"] = liked_by
|
| 225 |
+
|
| 226 |
+
elif req.action_type == "favorite":
|
| 227 |
+
favorited_by = post.get("favoritedBy", [])
|
| 228 |
+
if req.is_active and req.user_id not in favorited_by:
|
| 229 |
+
favorited_by.append(req.user_id)
|
| 230 |
+
post["favorites"] = post.get("favorites", 0) + 1
|
| 231 |
+
# 同步到用户收藏列表
|
| 232 |
+
user = users_db.get(req.user_id, {})
|
| 233 |
+
user_favs = user.get("favorited_posts", [])
|
| 234 |
+
if req.post_id not in user_favs:
|
| 235 |
+
user_favs.append(req.post_id)
|
| 236 |
+
user["favorited_posts"] = user_favs
|
| 237 |
+
users_db[req.user_id] = user
|
| 238 |
+
db.save_data("users.json", users_db)
|
| 239 |
+
elif not req.is_active and req.user_id in favorited_by:
|
| 240 |
+
favorited_by.remove(req.user_id)
|
| 241 |
+
post["favorites"] = max(0, post.get("favorites", 0) - 1)
|
| 242 |
+
# 从用户收藏列表移除
|
| 243 |
+
user = users_db.get(req.user_id, {})
|
| 244 |
+
user_favs = user.get("favorited_posts", [])
|
| 245 |
+
if req.post_id in user_favs:
|
| 246 |
+
user_favs.remove(req.post_id)
|
| 247 |
+
user["favorited_posts"] = user_favs
|
| 248 |
+
users_db[req.user_id] = user
|
| 249 |
+
db.save_data("users.json", users_db)
|
| 250 |
+
post["favoritedBy"] = favorited_by
|
| 251 |
+
|
| 252 |
+
db.save_data("posts.json", posts_db)
|
| 253 |
+
|
| 254 |
+
return {
|
| 255 |
+
"status": "success",
|
| 256 |
+
"data": {
|
| 257 |
+
"likes": post.get("likes", 0),
|
| 258 |
+
"favorites": post.get("favorites", 0),
|
| 259 |
+
"isLiked": req.user_id in post.get("likedBy", []),
|
| 260 |
+
"isFavorited": req.user_id in post.get("favoritedBy", [])
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# ==========================================
|
| 266 |
+
# 📊 获取用户的帖子列表
|
| 267 |
+
# ==========================================
|
| 268 |
+
@router.get("/api/posts/user/{account}")
|
| 269 |
+
async def get_user_posts(account: str):
|
| 270 |
+
"""
|
| 271 |
+
获取指定用户发布的帖子
|
| 272 |
+
"""
|
| 273 |
+
posts_db = db.load_data("posts.json", default_data=[])
|
| 274 |
+
|
| 275 |
+
user_posts = [p for p in posts_db if p.get("author") == account]
|
| 276 |
+
user_posts.sort(key=lambda x: x.get("createdAt", 0), reverse=True)
|
| 277 |
+
|
| 278 |
+
return {"status": "success", "data": user_posts}
|
router_tasks.py
ADDED
|
@@ -0,0 +1,1411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# router_tasks.py
|
| 2 |
+
# ==========================================
|
| 3 |
+
# 📝 任务榜路由模块
|
| 4 |
+
# ==========================================
|
| 5 |
+
# 作用:处理任务榜的发布、申请、指派、提交、验收等功能
|
| 6 |
+
# 关联文件:
|
| 7 |
+
# - 数据库连接.py (JSON数据库读写 tasks.json)
|
| 8 |
+
# - models.py (TaskCreate, TaskApply, TaskAssign 等)
|
| 9 |
+
# - router_wallet.py (订金/尾款支付)
|
| 10 |
+
# 前端调用:
|
| 11 |
+
# - 任务榜列表组件.js (获取任务列表)
|
| 12 |
+
# - 任务榜发布组件.js (发布新任务)
|
| 13 |
+
# - 任务榜详情组件.js (任务详情、申请/指派/提交/验收)
|
| 14 |
+
# ==========================================
|
| 15 |
+
# P2 增强功能:
|
| 16 |
+
# - 余额预检与冻结
|
| 17 |
+
# - 交易明细记录
|
| 18 |
+
# - 过期自动退款
|
| 19 |
+
# - 支付通知推送
|
| 20 |
+
# P3 增强功能:
|
| 21 |
+
# - 验收申诉机制
|
| 22 |
+
# - 管理员仲裁
|
| 23 |
+
# - 争议资金分配
|
| 24 |
+
# ==========================================
|
| 25 |
+
|
| 26 |
+
from fastapi import APIRouter, HTTPException
|
| 27 |
+
import 数据库连接 as db
|
| 28 |
+
from models import TaskCreate, TaskUpdate, TaskApply, TaskAssign, TaskSubmit, TaskAccept, TaskDispute, TaskDisputeResponse, TaskDisputeResolve
|
| 29 |
+
import time
|
| 30 |
+
import uuid
|
| 31 |
+
from datetime import datetime
|
| 32 |
+
|
| 33 |
+
# 创建子路由实例
|
| 34 |
+
router = APIRouter()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ==========================================
|
| 38 |
+
# 💰 P2增强:交易明细记录辅助函数
|
| 39 |
+
# ==========================================
|
| 40 |
+
def _record_transaction(account: str, tx_type: str, amount: int, related_task_id: str = None,
|
| 41 |
+
target_account: str = None, note: str = ""):
|
| 42 |
+
"""
|
| 43 |
+
记录交易明细
|
| 44 |
+
|
| 45 |
+
tx_type 类型:
|
| 46 |
+
- task_freeze: 发布任务冻结
|
| 47 |
+
- task_unfreeze: 取消任务解冻
|
| 48 |
+
- deposit_pay: 支付订金
|
| 49 |
+
- deposit_receive: 收到订金
|
| 50 |
+
- final_pay: 支付尾款
|
| 51 |
+
- final_receive: 收到尾款
|
| 52 |
+
- task_refund: 任务退款
|
| 53 |
+
- expired_refund: 过期退款
|
| 54 |
+
"""
|
| 55 |
+
transactions_db = db.load_data("transactions.json", default_data=[])
|
| 56 |
+
|
| 57 |
+
transaction = {
|
| 58 |
+
"id": f"tx_{uuid.uuid4().hex[:12]}",
|
| 59 |
+
"account": account,
|
| 60 |
+
"type": tx_type,
|
| 61 |
+
"amount": amount,
|
| 62 |
+
"related_task_id": related_task_id,
|
| 63 |
+
"target_account": target_account,
|
| 64 |
+
"note": note,
|
| 65 |
+
"createdAt": int(time.time())
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
transactions_db.insert(0, transaction)
|
| 69 |
+
db.save_data("transactions.json", transactions_db)
|
| 70 |
+
|
| 71 |
+
return transaction
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _send_task_notification(account: str, title: str, content: str, task_id: str = None):
|
| 75 |
+
"""
|
| 76 |
+
发送任务相关的系统通知
|
| 77 |
+
"""
|
| 78 |
+
messages_db = db.load_data("messages.json", default_data={})
|
| 79 |
+
|
| 80 |
+
if account not in messages_db:
|
| 81 |
+
messages_db[account] = []
|
| 82 |
+
|
| 83 |
+
notification = {
|
| 84 |
+
"id": f"msg_{uuid.uuid4().hex[:8]}",
|
| 85 |
+
"type": "task_notification",
|
| 86 |
+
"title": title,
|
| 87 |
+
"content": content,
|
| 88 |
+
"related_task_id": task_id,
|
| 89 |
+
"timestamp": int(time.time()),
|
| 90 |
+
"read": False
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
messages_db[account].insert(0, notification)
|
| 94 |
+
db.save_data("messages.json", messages_db)
|
| 95 |
+
|
| 96 |
+
return notification
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ==========================================
|
| 100 |
+
# 📖 获取任务榜列表(仅显示未过期+招募中的任务)
|
| 101 |
+
# ==========================================
|
| 102 |
+
@router.get("/api/tasks")
|
| 103 |
+
async def get_tasks(sort: str = "time", page: int = 1, page_size: int = 20):
|
| 104 |
+
"""
|
| 105 |
+
获取任务列表
|
| 106 |
+
P2增强:过期自动退款
|
| 107 |
+
|
| 108 |
+
查询参数:
|
| 109 |
+
- sort: 排序方式 (time=最新, price=价格最高)
|
| 110 |
+
- page: 页码
|
| 111 |
+
- page_size: 每页数量
|
| 112 |
+
"""
|
| 113 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 114 |
+
users_db = db.load_data("users.json", default_data={})
|
| 115 |
+
current_time = int(time.time())
|
| 116 |
+
|
| 117 |
+
# 过滤:只显示 open 状态且未过期的任务
|
| 118 |
+
visible_tasks = []
|
| 119 |
+
users_updated = False
|
| 120 |
+
|
| 121 |
+
for task in tasks_db:
|
| 122 |
+
# 检查是否过期
|
| 123 |
+
deadline_ts = _parse_deadline(task.get("deadline", ""))
|
| 124 |
+
if deadline_ts and deadline_ts < current_time:
|
| 125 |
+
# P2增强:过期自动退款
|
| 126 |
+
if task.get("status") in ["open", "assigned"]:
|
| 127 |
+
_handle_task_expiry(task, users_db)
|
| 128 |
+
users_updated = True
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
# 只显示招募中的任务
|
| 132 |
+
if task.get("status") == "open":
|
| 133 |
+
visible_tasks.append(task)
|
| 134 |
+
|
| 135 |
+
# 保存状态更新
|
| 136 |
+
db.save_data("tasks.json", tasks_db)
|
| 137 |
+
if users_updated:
|
| 138 |
+
db.save_data("users.json", users_db)
|
| 139 |
+
|
| 140 |
+
# 排序
|
| 141 |
+
if sort == "price":
|
| 142 |
+
visible_tasks.sort(key=lambda x: x.get("totalPrice", 0), reverse=True)
|
| 143 |
+
else:
|
| 144 |
+
visible_tasks.sort(key=lambda x: x.get("createdAt", 0), reverse=True)
|
| 145 |
+
|
| 146 |
+
# 分页
|
| 147 |
+
start = (page - 1) * page_size
|
| 148 |
+
end = start + page_size
|
| 149 |
+
paginated = visible_tasks[start:end]
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"status": "success",
|
| 153 |
+
"data": paginated,
|
| 154 |
+
"total": len(visible_tasks),
|
| 155 |
+
"page": page,
|
| 156 |
+
"page_size": page_size
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _handle_task_expiry(task: dict, users_db: dict):
|
| 161 |
+
"""
|
| 162 |
+
P2增强:处理任务过期退款
|
| 163 |
+
"""
|
| 164 |
+
publisher = task.get("publisher")
|
| 165 |
+
assignee = task.get("assignee")
|
| 166 |
+
status = task.get("status")
|
| 167 |
+
|
| 168 |
+
publisher_info = users_db.get(publisher, {})
|
| 169 |
+
|
| 170 |
+
if status == "open":
|
| 171 |
+
# 招募中过期:解冻全部金额
|
| 172 |
+
frozen_amount = task.get("frozenAmount", task.get("totalPrice", 0))
|
| 173 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - frozen_amount)
|
| 174 |
+
users_db[publisher] = publisher_info
|
| 175 |
+
|
| 176 |
+
_record_transaction(
|
| 177 |
+
account=publisher,
|
| 178 |
+
tx_type="expired_refund",
|
| 179 |
+
amount=frozen_amount,
|
| 180 |
+
related_task_id=task.get("id"),
|
| 181 |
+
note=f"任务过期解冻: {task['title'][:20]}"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
_send_task_notification(
|
| 185 |
+
account=publisher,
|
| 186 |
+
title="⏰ 任务已过期",
|
| 187 |
+
content=f"任务『{task['title']}』已过期,冻结的 {frozen_amount} 积分已解冻。",
|
| 188 |
+
task_id=task.get("id")
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
elif status == "assigned":
|
| 192 |
+
# 已指派但未完成过期
|
| 193 |
+
if task.get("depositPaid"):
|
| 194 |
+
# 已支付订金:从接单者扣回订金,解冻尾款
|
| 195 |
+
deposit_amount = task.get("depositAmount", 0)
|
| 196 |
+
final_payment = task.get("finalPayment", 0)
|
| 197 |
+
|
| 198 |
+
if assignee in users_db:
|
| 199 |
+
users_db[assignee]["balance"] = max(0, users_db[assignee].get("balance", 0) - deposit_amount)
|
| 200 |
+
|
| 201 |
+
publisher_info["balance"] = publisher_info.get("balance", 0) + deposit_amount
|
| 202 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - final_payment)
|
| 203 |
+
users_db[publisher] = publisher_info
|
| 204 |
+
|
| 205 |
+
_record_transaction(
|
| 206 |
+
account=publisher,
|
| 207 |
+
tx_type="expired_refund",
|
| 208 |
+
amount=deposit_amount,
|
| 209 |
+
related_task_id=task.get("id"),
|
| 210 |
+
target_account=assignee,
|
| 211 |
+
note=f"任务过期收回订金: {task['title'][:20]}"
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
_send_task_notification(
|
| 215 |
+
account=publisher,
|
| 216 |
+
title="⏰ 任务已过期",
|
| 217 |
+
content=f"任务『{task['title']}』已过期,订金 {deposit_amount} 积分已退回。",
|
| 218 |
+
task_id=task.get("id")
|
| 219 |
+
)
|
| 220 |
+
_send_task_notification(
|
| 221 |
+
account=assignee,
|
| 222 |
+
title="⏰ 任务已过期",
|
| 223 |
+
content=f"任务『{task['title']}』已过期,订金 {deposit_amount} 积分已退回发布者。",
|
| 224 |
+
task_id=task.get("id")
|
| 225 |
+
)
|
| 226 |
+
else:
|
| 227 |
+
# 未支付订金:解冻全部金额
|
| 228 |
+
frozen_amount = task.get("frozenAmount", task.get("totalPrice", 0))
|
| 229 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - frozen_amount)
|
| 230 |
+
users_db[publisher] = publisher_info
|
| 231 |
+
|
| 232 |
+
_record_transaction(
|
| 233 |
+
account=publisher,
|
| 234 |
+
tx_type="expired_refund",
|
| 235 |
+
amount=frozen_amount,
|
| 236 |
+
related_task_id=task.get("id"),
|
| 237 |
+
note=f"任务过期解冻: {task['title'][:20]}"
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
_send_task_notification(
|
| 241 |
+
account=publisher,
|
| 242 |
+
title="⏰ 任务已过期",
|
| 243 |
+
content=f"任务『{task['title']}』已过期,冻结的 {frozen_amount} 积分已解冻。",
|
| 244 |
+
task_id=task.get("id")
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
task["status"] = "expired"
|
| 248 |
+
task["expiredAt"] = int(time.time())
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# ==========================================
|
| 252 |
+
# 📖 获取单个任务详情
|
| 253 |
+
# ==========================================
|
| 254 |
+
@router.get("/api/tasks/{task_id}")
|
| 255 |
+
async def get_task_detail(task_id: str):
|
| 256 |
+
"""
|
| 257 |
+
获取任务详情
|
| 258 |
+
"""
|
| 259 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 260 |
+
|
| 261 |
+
task = next((t for t in tasks_db if t.get("id") == task_id), None)
|
| 262 |
+
if not task:
|
| 263 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 264 |
+
|
| 265 |
+
return {"status": "success", "data": task}
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ==========================================
|
| 269 |
+
# ✏️ 发布新任务(P2增强:余额预检+冻结)
|
| 270 |
+
# ==========================================
|
| 271 |
+
@router.post("/api/tasks")
|
| 272 |
+
async def create_task(task_data: TaskCreate):
|
| 273 |
+
"""
|
| 274 |
+
发布新任务
|
| 275 |
+
P2增强:发布时预检余额并冻结总金额
|
| 276 |
+
"""
|
| 277 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 278 |
+
users_db = db.load_data("users.json", default_data={})
|
| 279 |
+
|
| 280 |
+
# 获取发布者信息
|
| 281 |
+
publisher_info = users_db.get(task_data.publisher, {})
|
| 282 |
+
|
| 283 |
+
# 💰 P2增强:余额预检
|
| 284 |
+
current_balance = publisher_info.get("balance", 0)
|
| 285 |
+
frozen_balance = publisher_info.get("frozen_balance", 0)
|
| 286 |
+
available_balance = current_balance - frozen_balance
|
| 287 |
+
|
| 288 |
+
if available_balance < task_data.totalPrice:
|
| 289 |
+
raise HTTPException(
|
| 290 |
+
status_code=400,
|
| 291 |
+
detail=f"可用余额不足!当前可用 {available_balance} 积分,任务需要 {task_data.totalPrice} 积分"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# 计算订金和尾款
|
| 295 |
+
deposit_amount = int(task_data.totalPrice * task_data.depositRatio / 100)
|
| 296 |
+
final_payment = task_data.totalPrice - deposit_amount
|
| 297 |
+
|
| 298 |
+
# 💰 P2增强:冻结总金额
|
| 299 |
+
publisher_info["frozen_balance"] = frozen_balance + task_data.totalPrice
|
| 300 |
+
users_db[task_data.publisher] = publisher_info
|
| 301 |
+
db.save_data("users.json", users_db)
|
| 302 |
+
|
| 303 |
+
# 记录冻结交易
|
| 304 |
+
_record_transaction(
|
| 305 |
+
account=task_data.publisher,
|
| 306 |
+
tx_type="task_freeze",
|
| 307 |
+
amount=task_data.totalPrice,
|
| 308 |
+
note=f"发布任务冻结: {task_data.title[:20]}"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# 构建新任务对象
|
| 312 |
+
task_id = f"task_{uuid.uuid4().hex[:12]}"
|
| 313 |
+
new_task = {
|
| 314 |
+
"id": task_id,
|
| 315 |
+
"type": "task",
|
| 316 |
+
"title": task_data.title,
|
| 317 |
+
"description": task_data.description,
|
| 318 |
+
"referenceImages": task_data.referenceImages or [],
|
| 319 |
+
"referenceLink": task_data.referenceLink or "",
|
| 320 |
+
|
| 321 |
+
# 💰 价格与订金
|
| 322 |
+
"totalPrice": task_data.totalPrice,
|
| 323 |
+
"depositRatio": task_data.depositRatio,
|
| 324 |
+
"depositAmount": deposit_amount,
|
| 325 |
+
"finalPayment": final_payment,
|
| 326 |
+
"frozenAmount": task_data.totalPrice, # P2: 记录冻结金额
|
| 327 |
+
|
| 328 |
+
# ⏰ 时间控制
|
| 329 |
+
"deadline": task_data.deadline,
|
| 330 |
+
"createdAt": int(time.time()),
|
| 331 |
+
|
| 332 |
+
# 👤 参与者
|
| 333 |
+
"publisher": task_data.publisher,
|
| 334 |
+
"publisherName": publisher_info.get("name", task_data.publisher),
|
| 335 |
+
"publisherAvatar": publisher_info.get("avatarDataUrl", ""),
|
| 336 |
+
"applicants": [], # 申请者列表 [{account, name, avatar, message, appliedAt}]
|
| 337 |
+
"assignee": None, # 被选中的接单者
|
| 338 |
+
"assigneeName": None,
|
| 339 |
+
"assigneeAvatar": None,
|
| 340 |
+
|
| 341 |
+
# 📊 状态机
|
| 342 |
+
"status": "open", # open/assigned/in_progress/submitted/completed/expired/cancelled
|
| 343 |
+
"depositPaid": False, # 订金已支付
|
| 344 |
+
"finalPaid": False, # 尾款已支付
|
| 345 |
+
|
| 346 |
+
# 📁 交付物
|
| 347 |
+
"deliverables": [], # 接单者提交的成果
|
| 348 |
+
"deliverNote": "", # 交付备注
|
| 349 |
+
"publisherFeedback": "" # 发布者反馈
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
tasks_db.insert(0, new_task)
|
| 353 |
+
db.save_data("tasks.json", tasks_db)
|
| 354 |
+
|
| 355 |
+
return {"status": "success", "data": new_task, "message": f"任务发布成功,已冻结 {task_data.totalPrice} 积分"}
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ==========================================
|
| 359 |
+
# 🙋 申请接单
|
| 360 |
+
# ==========================================
|
| 361 |
+
@router.post("/api/tasks/apply")
|
| 362 |
+
async def apply_task(req: TaskApply):
|
| 363 |
+
"""
|
| 364 |
+
接单者申请接单
|
| 365 |
+
"""
|
| 366 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 367 |
+
users_db = db.load_data("users.json", default_data={})
|
| 368 |
+
|
| 369 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == req.task_id), None)
|
| 370 |
+
if task_idx is None:
|
| 371 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 372 |
+
|
| 373 |
+
task = tasks_db[task_idx]
|
| 374 |
+
|
| 375 |
+
# 状态检查
|
| 376 |
+
if task.get("status") != "open":
|
| 377 |
+
raise HTTPException(status_code=400, detail="任务不在招募中")
|
| 378 |
+
|
| 379 |
+
# 不能申请自己发布的任务
|
| 380 |
+
if task.get("publisher") == req.applicant:
|
| 381 |
+
raise HTTPException(status_code=400, detail="不能申请自己发布的任务")
|
| 382 |
+
|
| 383 |
+
# 检查是否已申请
|
| 384 |
+
applicants = task.get("applicants", [])
|
| 385 |
+
if any(a.get("account") == req.applicant for a in applicants):
|
| 386 |
+
raise HTTPException(status_code=400, detail="您已申请过此任务")
|
| 387 |
+
|
| 388 |
+
# 获取申请者信息
|
| 389 |
+
applicant_info = users_db.get(req.applicant, {})
|
| 390 |
+
|
| 391 |
+
# 添加申请
|
| 392 |
+
applicants.append({
|
| 393 |
+
"account": req.applicant,
|
| 394 |
+
"name": applicant_info.get("name", req.applicant),
|
| 395 |
+
"avatar": applicant_info.get("avatarDataUrl", ""),
|
| 396 |
+
"message": req.message or "",
|
| 397 |
+
"appliedAt": int(time.time())
|
| 398 |
+
})
|
| 399 |
+
task["applicants"] = applicants
|
| 400 |
+
|
| 401 |
+
db.save_data("tasks.json", tasks_db)
|
| 402 |
+
|
| 403 |
+
# TODO: 发送通知给发布者
|
| 404 |
+
|
| 405 |
+
return {"status": "success", "message": "申请成功,等待发布者选择"}
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# ==========================================
|
| 409 |
+
# 👆 发布者指派接单者
|
| 410 |
+
# ==========================================
|
| 411 |
+
@router.post("/api/tasks/assign")
|
| 412 |
+
async def assign_task(req: TaskAssign):
|
| 413 |
+
"""
|
| 414 |
+
发布者指派接单者
|
| 415 |
+
"""
|
| 416 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 417 |
+
users_db = db.load_data("users.json", default_data={})
|
| 418 |
+
|
| 419 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == req.task_id), None)
|
| 420 |
+
if task_idx is None:
|
| 421 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 422 |
+
|
| 423 |
+
task = tasks_db[task_idx]
|
| 424 |
+
|
| 425 |
+
# 权限校验
|
| 426 |
+
if task.get("publisher") != req.publisher:
|
| 427 |
+
raise HTTPException(status_code=403, detail="无权操作此任务")
|
| 428 |
+
|
| 429 |
+
# 状态检查
|
| 430 |
+
if task.get("status") != "open":
|
| 431 |
+
raise HTTPException(status_code=400, detail="任务已不在招募中")
|
| 432 |
+
|
| 433 |
+
# 检查指派的人是否在申请列表中
|
| 434 |
+
applicants = task.get("applicants", [])
|
| 435 |
+
assignee_info = next((a for a in applicants if a.get("account") == req.assignee), None)
|
| 436 |
+
if not assignee_info:
|
| 437 |
+
raise HTTPException(status_code=400, detail="该用户未申请此任务")
|
| 438 |
+
|
| 439 |
+
# 更新任务状态
|
| 440 |
+
task["assignee"] = req.assignee
|
| 441 |
+
task["assigneeName"] = assignee_info.get("name", req.assignee)
|
| 442 |
+
task["assigneeAvatar"] = assignee_info.get("avatar", "")
|
| 443 |
+
task["status"] = "assigned" # 等待支付订金
|
| 444 |
+
|
| 445 |
+
db.save_data("tasks.json", tasks_db)
|
| 446 |
+
|
| 447 |
+
# P2增强:发送通知给接单者
|
| 448 |
+
_send_task_notification(
|
| 449 |
+
account=req.assignee,
|
| 450 |
+
title="🎉 您已被选中接单",
|
| 451 |
+
content=f"您已被选中接单任务『{task['title']}』,请等待发布者支付订金后开始工作。",
|
| 452 |
+
task_id=task["id"]
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
return {"status": "success", "message": "已指派接单者,请支付订金"}
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
# ==========================================
|
| 459 |
+
# 💰 支付订金(P2增强:从冻结余额扣款+交易记录+通知)
|
| 460 |
+
# ==========================================
|
| 461 |
+
@router.post("/api/tasks/{task_id}/pay_deposit")
|
| 462 |
+
async def pay_deposit(task_id: str, publisher: str):
|
| 463 |
+
"""
|
| 464 |
+
发布者支付订金
|
| 465 |
+
P2增强:从冻结余额中扣除订金
|
| 466 |
+
"""
|
| 467 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 468 |
+
users_db = db.load_data("users.json", default_data={})
|
| 469 |
+
|
| 470 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == task_id), None)
|
| 471 |
+
if task_idx is None:
|
| 472 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 473 |
+
|
| 474 |
+
task = tasks_db[task_idx]
|
| 475 |
+
|
| 476 |
+
# 权限校验
|
| 477 |
+
if task.get("publisher") != publisher:
|
| 478 |
+
raise HTTPException(status_code=403, detail="无权操作此任务")
|
| 479 |
+
|
| 480 |
+
# 状态检查
|
| 481 |
+
if task.get("status") != "assigned":
|
| 482 |
+
raise HTTPException(status_code=400, detail="当前状态不允许支付订金")
|
| 483 |
+
|
| 484 |
+
if task.get("depositPaid"):
|
| 485 |
+
raise HTTPException(status_code=400, detail="订金已支付")
|
| 486 |
+
|
| 487 |
+
deposit_amount = task.get("depositAmount", 0)
|
| 488 |
+
assignee = task.get("assignee")
|
| 489 |
+
publisher_info = users_db.get(publisher, {})
|
| 490 |
+
|
| 491 |
+
# P2增强:从冻结余额中扣除订金
|
| 492 |
+
frozen_balance = publisher_info.get("frozen_balance", 0)
|
| 493 |
+
current_balance = publisher_info.get("balance", 0)
|
| 494 |
+
|
| 495 |
+
# 订金从冻结金额中支付(已在发布时冻结)
|
| 496 |
+
# 扣减实际余额,同时减少冻结金额
|
| 497 |
+
publisher_info["balance"] = current_balance - deposit_amount
|
| 498 |
+
publisher_info["frozen_balance"] = frozen_balance - deposit_amount
|
| 499 |
+
users_db[publisher] = publisher_info
|
| 500 |
+
|
| 501 |
+
# 订金转入接单者账户
|
| 502 |
+
if assignee not in users_db:
|
| 503 |
+
users_db[assignee] = {"balance": 0, "frozen_balance": 0}
|
| 504 |
+
users_db[assignee]["balance"] = users_db[assignee].get("balance", 0) + deposit_amount
|
| 505 |
+
|
| 506 |
+
# 更新任务状态
|
| 507 |
+
task["depositPaid"] = True
|
| 508 |
+
task["status"] = "in_progress"
|
| 509 |
+
task["depositPaidAt"] = int(time.time())
|
| 510 |
+
|
| 511 |
+
db.save_data("tasks.json", tasks_db)
|
| 512 |
+
db.save_data("users.json", users_db)
|
| 513 |
+
|
| 514 |
+
# P2增强:记录交易明细
|
| 515 |
+
_record_transaction(
|
| 516 |
+
account=publisher,
|
| 517 |
+
tx_type="deposit_pay",
|
| 518 |
+
amount=deposit_amount,
|
| 519 |
+
related_task_id=task_id,
|
| 520 |
+
target_account=assignee,
|
| 521 |
+
note=f"支付订金: {task['title'][:20]}"
|
| 522 |
+
)
|
| 523 |
+
_record_transaction(
|
| 524 |
+
account=assignee,
|
| 525 |
+
tx_type="deposit_receive",
|
| 526 |
+
amount=deposit_amount,
|
| 527 |
+
related_task_id=task_id,
|
| 528 |
+
target_account=publisher,
|
| 529 |
+
note=f"收到订金: {task['title'][:20]}"
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
# P2增强:发送通知
|
| 533 |
+
_send_task_notification(
|
| 534 |
+
account=assignee,
|
| 535 |
+
title="💰 订金已到账",
|
| 536 |
+
content=f"任务『{task['title']}』的订金 {deposit_amount} 积分已到账,请尽快开始工作!",
|
| 537 |
+
task_id=task_id
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
return {"status": "success", "message": f"订金 {deposit_amount} 积分已支付,任务开始进行"}
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
# ==========================================
|
| 544 |
+
# 📤 接单者提交成果
|
| 545 |
+
# ==========================================
|
| 546 |
+
@router.post("/api/tasks/submit")
|
| 547 |
+
async def submit_deliverables(req: TaskSubmit):
|
| 548 |
+
"""
|
| 549 |
+
接单者提交成果
|
| 550 |
+
"""
|
| 551 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 552 |
+
|
| 553 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == req.task_id), None)
|
| 554 |
+
if task_idx is None:
|
| 555 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 556 |
+
|
| 557 |
+
task = tasks_db[task_idx]
|
| 558 |
+
|
| 559 |
+
# 权限校验
|
| 560 |
+
if task.get("assignee") != req.assignee:
|
| 561 |
+
raise HTTPException(status_code=403, detail="您不是此任务的接单者")
|
| 562 |
+
|
| 563 |
+
# 状态检查
|
| 564 |
+
if task.get("status") != "in_progress":
|
| 565 |
+
raise HTTPException(status_code=400, detail="当前状态不允许提交成果")
|
| 566 |
+
|
| 567 |
+
# 更新交付物
|
| 568 |
+
task["deliverables"] = req.deliverables
|
| 569 |
+
task["deliverNote"] = req.note or ""
|
| 570 |
+
task["status"] = "submitted"
|
| 571 |
+
task["submittedAt"] = int(time.time())
|
| 572 |
+
|
| 573 |
+
db.save_data("tasks.json", tasks_db)
|
| 574 |
+
|
| 575 |
+
# P2增强:发送通知给发布者
|
| 576 |
+
_send_task_notification(
|
| 577 |
+
account=task.get("publisher"),
|
| 578 |
+
title="📥 任务成果已提交",
|
| 579 |
+
content=f"接单者已提交任务『{task['title']}』的成果,请查看并验收。",
|
| 580 |
+
task_id=req.task_id
|
| 581 |
+
)
|
| 582 |
+
|
| 583 |
+
return {"status": "success", "message": "成果已提交,等待发布者验收"}
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
# ==========================================
|
| 587 |
+
# ✅ 发布者验收(P2增强:从冻结余额支付尾款+交易记录+通知)
|
| 588 |
+
# ==========================================
|
| 589 |
+
@router.post("/api/tasks/accept")
|
| 590 |
+
async def accept_task(req: TaskAccept):
|
| 591 |
+
"""
|
| 592 |
+
发布者验收成果
|
| 593 |
+
P2增强:尾款从冻结余额支付
|
| 594 |
+
"""
|
| 595 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 596 |
+
users_db = db.load_data("users.json", default_data={})
|
| 597 |
+
|
| 598 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == req.task_id), None)
|
| 599 |
+
if task_idx is None:
|
| 600 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 601 |
+
|
| 602 |
+
task = tasks_db[task_idx]
|
| 603 |
+
|
| 604 |
+
# 权限校验
|
| 605 |
+
if task.get("publisher") != req.publisher:
|
| 606 |
+
raise HTTPException(status_code=403, detail="无权操作此任务")
|
| 607 |
+
|
| 608 |
+
# 状态检查
|
| 609 |
+
if task.get("status") != "submitted":
|
| 610 |
+
raise HTTPException(status_code=400, detail="当前状态不允许验收")
|
| 611 |
+
|
| 612 |
+
task["publisherFeedback"] = req.feedback or ""
|
| 613 |
+
publisher = task.get("publisher")
|
| 614 |
+
assignee = task.get("assignee")
|
| 615 |
+
|
| 616 |
+
if req.is_accepted:
|
| 617 |
+
# 验收通过:支付尾款
|
| 618 |
+
final_payment = task.get("finalPayment", 0)
|
| 619 |
+
publisher_info = users_db.get(publisher, {})
|
| 620 |
+
|
| 621 |
+
# P2增强:尾款从冻结余额中支付
|
| 622 |
+
frozen_balance = publisher_info.get("frozen_balance", 0)
|
| 623 |
+
current_balance = publisher_info.get("balance", 0)
|
| 624 |
+
|
| 625 |
+
# 扣减实际余额,同时减少冻结金额
|
| 626 |
+
publisher_info["balance"] = current_balance - final_payment
|
| 627 |
+
publisher_info["frozen_balance"] = frozen_balance - final_payment
|
| 628 |
+
users_db[publisher] = publisher_info
|
| 629 |
+
|
| 630 |
+
# 尾款转入接单者账户
|
| 631 |
+
if assignee not in users_db:
|
| 632 |
+
users_db[assignee] = {"balance": 0, "frozen_balance": 0}
|
| 633 |
+
users_db[assignee]["balance"] = users_db[assignee].get("balance", 0) + final_payment
|
| 634 |
+
|
| 635 |
+
# 更新任务状态
|
| 636 |
+
task["finalPaid"] = True
|
| 637 |
+
task["status"] = "completed"
|
| 638 |
+
task["completedAt"] = int(time.time())
|
| 639 |
+
|
| 640 |
+
db.save_data("users.json", users_db)
|
| 641 |
+
|
| 642 |
+
# P2增强:记录交易明细
|
| 643 |
+
_record_transaction(
|
| 644 |
+
account=publisher,
|
| 645 |
+
tx_type="final_pay",
|
| 646 |
+
amount=final_payment,
|
| 647 |
+
related_task_id=req.task_id,
|
| 648 |
+
target_account=assignee,
|
| 649 |
+
note=f"支付尾款: {task['title'][:20]}"
|
| 650 |
+
)
|
| 651 |
+
_record_transaction(
|
| 652 |
+
account=assignee,
|
| 653 |
+
tx_type="final_receive",
|
| 654 |
+
amount=final_payment,
|
| 655 |
+
related_task_id=req.task_id,
|
| 656 |
+
target_account=publisher,
|
| 657 |
+
note=f"收到尾款: {task['title'][:20]}"
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
# P2增强:发送通知
|
| 661 |
+
total_earned = task.get("depositAmount", 0) + final_payment
|
| 662 |
+
_send_task_notification(
|
| 663 |
+
account=assignee,
|
| 664 |
+
title="🎉 任务已完成",
|
| 665 |
+
content=f"任务『{task['title']}』已验收通过!尾款 {final_payment} 积分已到账,共计收入 {total_earned} 积分。",
|
| 666 |
+
task_id=req.task_id
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
message = f"验收通过!尾款 {final_payment} 积分已支付给接单者"
|
| 670 |
+
else:
|
| 671 |
+
# 验收不通过:任务回到进行中状态
|
| 672 |
+
task["status"] = "in_progress"
|
| 673 |
+
task["deliverables"] = [] # 清空交付物
|
| 674 |
+
|
| 675 |
+
# P2增强:发送通知
|
| 676 |
+
_send_task_notification(
|
| 677 |
+
account=assignee,
|
| 678 |
+
title="⚠️ 验收未通过",
|
| 679 |
+
content=f"任务『{task['title']}』验收未通过,请查看反馈并重新提交。反馈:{req.feedback or '无'}",
|
| 680 |
+
task_id=req.task_id
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
message = "验收不通过,请接单者重新提交"
|
| 684 |
+
|
| 685 |
+
db.save_data("tasks.json", tasks_db)
|
| 686 |
+
|
| 687 |
+
return {"status": "success", "message": message}
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
# ==========================================
|
| 691 |
+
# ❌ 取消任务(P2增强:解冻余额+交易记录+通知)
|
| 692 |
+
# ==========================================
|
| 693 |
+
@router.post("/api/tasks/{task_id}/cancel")
|
| 694 |
+
async def cancel_task(task_id: str, publisher: str):
|
| 695 |
+
"""
|
| 696 |
+
发布者取消任务
|
| 697 |
+
P2增强:取消时解冻余额,退还订金
|
| 698 |
+
"""
|
| 699 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 700 |
+
users_db = db.load_data("users.json", default_data={})
|
| 701 |
+
|
| 702 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == task_id), None)
|
| 703 |
+
if task_idx is None:
|
| 704 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 705 |
+
|
| 706 |
+
task = tasks_db[task_idx]
|
| 707 |
+
|
| 708 |
+
# 权限校验
|
| 709 |
+
if task.get("publisher") != publisher:
|
| 710 |
+
raise HTTPException(status_code=403, detail="无权操作此任务")
|
| 711 |
+
|
| 712 |
+
status = task.get("status")
|
| 713 |
+
|
| 714 |
+
# 只有 open 和 assigned 状态可以取消
|
| 715 |
+
if status not in ["open", "assigned"]:
|
| 716 |
+
raise HTTPException(status_code=400, detail="任务进行中或已完成,无法取消")
|
| 717 |
+
|
| 718 |
+
publisher_info = users_db.get(publisher, {})
|
| 719 |
+
frozen_amount = task.get("frozenAmount", task.get("totalPrice", 0))
|
| 720 |
+
assignee = task.get("assignee")
|
| 721 |
+
|
| 722 |
+
# P2增强:解冻余额
|
| 723 |
+
if status == "open":
|
| 724 |
+
# 招募中取消:解冻全部金额
|
| 725 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - frozen_amount)
|
| 726 |
+
users_db[publisher] = publisher_info
|
| 727 |
+
|
| 728 |
+
_record_transaction(
|
| 729 |
+
account=publisher,
|
| 730 |
+
tx_type="task_unfreeze",
|
| 731 |
+
amount=frozen_amount,
|
| 732 |
+
related_task_id=task_id,
|
| 733 |
+
note=f"取消任务解冻: {task['title'][:20]}"
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
elif status == "assigned":
|
| 737 |
+
if task.get("depositPaid"):
|
| 738 |
+
# 已支付订金:从接单者扣回订金,解冻剩余尾款
|
| 739 |
+
deposit_amount = task.get("depositAmount", 0)
|
| 740 |
+
final_payment = task.get("finalPayment", 0)
|
| 741 |
+
|
| 742 |
+
# 从接单者扣除订金
|
| 743 |
+
if assignee in users_db:
|
| 744 |
+
users_db[assignee]["balance"] = max(0, users_db[assignee].get("balance", 0) - deposit_amount)
|
| 745 |
+
|
| 746 |
+
# 退还给发布者
|
| 747 |
+
publisher_info["balance"] = publisher_info.get("balance", 0) + deposit_amount
|
| 748 |
+
# 解冻剩余尾款
|
| 749 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - final_payment)
|
| 750 |
+
users_db[publisher] = publisher_info
|
| 751 |
+
|
| 752 |
+
# 记录退款交易
|
| 753 |
+
_record_transaction(
|
| 754 |
+
account=assignee,
|
| 755 |
+
tx_type="task_refund",
|
| 756 |
+
amount=-deposit_amount,
|
| 757 |
+
related_task_id=task_id,
|
| 758 |
+
target_account=publisher,
|
| 759 |
+
note=f"任务取消退回订金: {task['title'][:20]}"
|
| 760 |
+
)
|
| 761 |
+
_record_transaction(
|
| 762 |
+
account=publisher,
|
| 763 |
+
tx_type="task_refund",
|
| 764 |
+
amount=deposit_amount,
|
| 765 |
+
related_task_id=task_id,
|
| 766 |
+
target_account=assignee,
|
| 767 |
+
note=f"任务取消收回订金: {task['title'][:20]}"
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
# 通知接单者
|
| 771 |
+
_send_task_notification(
|
| 772 |
+
account=assignee,
|
| 773 |
+
title="⚠️ 任务已取消",
|
| 774 |
+
content=f"任务『{task['title']}』已被发布者取消,订金 {deposit_amount} 积分已退回。",
|
| 775 |
+
task_id=task_id
|
| 776 |
+
)
|
| 777 |
+
else:
|
| 778 |
+
# 未支付订金:解冻全部金额
|
| 779 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - frozen_amount)
|
| 780 |
+
users_db[publisher] = publisher_info
|
| 781 |
+
|
| 782 |
+
_record_transaction(
|
| 783 |
+
account=publisher,
|
| 784 |
+
tx_type="task_unfreeze",
|
| 785 |
+
amount=frozen_amount,
|
| 786 |
+
related_task_id=task_id,
|
| 787 |
+
note=f"取消任务解冻: {task['title'][:20]}"
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
# 通知接单者
|
| 791 |
+
if assignee:
|
| 792 |
+
_send_task_notification(
|
| 793 |
+
account=assignee,
|
| 794 |
+
title="⚠️ 任务已取消",
|
| 795 |
+
content=f"任务『{task['title']}』已被发布者取消。",
|
| 796 |
+
task_id=task_id
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
db.save_data("users.json", users_db)
|
| 800 |
+
|
| 801 |
+
task["status"] = "cancelled"
|
| 802 |
+
task["cancelledAt"] = int(time.time())
|
| 803 |
+
|
| 804 |
+
db.save_data("tasks.json", tasks_db)
|
| 805 |
+
|
| 806 |
+
return {"status": "success", "message": "任务已取消"}
|
| 807 |
+
|
| 808 |
+
|
| 809 |
+
# ==========================================
|
| 810 |
+
# 📊 获取用户相关的任务
|
| 811 |
+
# ==========================================
|
| 812 |
+
@router.get("/api/tasks/user/{account}")
|
| 813 |
+
async def get_user_tasks(account: str, role: str = "all"):
|
| 814 |
+
"""
|
| 815 |
+
获取用户发布或参与的任务
|
| 816 |
+
|
| 817 |
+
查询参数:
|
| 818 |
+
- role: publisher=我发布的, assignee=我接的, all=全部
|
| 819 |
+
"""
|
| 820 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 821 |
+
|
| 822 |
+
user_tasks = []
|
| 823 |
+
for task in tasks_db:
|
| 824 |
+
is_publisher = task.get("publisher") == account
|
| 825 |
+
is_assignee = task.get("assignee") == account
|
| 826 |
+
is_applicant = any(a.get("account") == account for a in task.get("applicants", []))
|
| 827 |
+
|
| 828 |
+
if role == "publisher" and is_publisher:
|
| 829 |
+
user_tasks.append({**task, "userRole": "publisher"})
|
| 830 |
+
elif role == "assignee" and (is_assignee or is_applicant):
|
| 831 |
+
user_tasks.append({**task, "userRole": "assignee" if is_assignee else "applicant"})
|
| 832 |
+
elif role == "all" and (is_publisher or is_assignee or is_applicant):
|
| 833 |
+
user_tasks.append({
|
| 834 |
+
**task,
|
| 835 |
+
"userRole": "publisher" if is_publisher else ("assignee" if is_assignee else "applicant")
|
| 836 |
+
})
|
| 837 |
+
|
| 838 |
+
user_tasks.sort(key=lambda x: x.get("createdAt", 0), reverse=True)
|
| 839 |
+
|
| 840 |
+
return {"status": "success", "data": user_tasks}
|
| 841 |
+
|
| 842 |
+
|
| 843 |
+
# ==========================================
|
| 844 |
+
# 🔧 辅助函数
|
| 845 |
+
# ==========================================
|
| 846 |
+
def _parse_deadline(deadline_str: str) -> int:
|
| 847 |
+
"""
|
| 848 |
+
解析截止时间字符串为时间戳
|
| 849 |
+
"""
|
| 850 |
+
if not deadline_str:
|
| 851 |
+
return 0
|
| 852 |
+
try:
|
| 853 |
+
# 尝试多种格式
|
| 854 |
+
for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"]:
|
| 855 |
+
try:
|
| 856 |
+
dt = datetime.strptime(deadline_str, fmt)
|
| 857 |
+
return int(dt.timestamp())
|
| 858 |
+
except ValueError:
|
| 859 |
+
continue
|
| 860 |
+
return 0
|
| 861 |
+
except:
|
| 862 |
+
return 0
|
| 863 |
+
|
| 864 |
+
|
| 865 |
+
# ==========================================
|
| 866 |
+
# 💰 P2增强:获取用户交易明细
|
| 867 |
+
# ==========================================
|
| 868 |
+
@router.get("/api/transactions/user/{account}")
|
| 869 |
+
async def get_user_transactions(account: str, tx_type: str = "all", page: int = 1, page_size: int = 20):
|
| 870 |
+
"""
|
| 871 |
+
获取用户交易明细
|
| 872 |
+
|
| 873 |
+
查询参数:
|
| 874 |
+
- tx_type: 交易类型过滤 (all/deposit/final/freeze/refund)
|
| 875 |
+
- page: 页码
|
| 876 |
+
- page_size: 每页数量
|
| 877 |
+
"""
|
| 878 |
+
transactions_db = db.load_data("transactions.json", default_data=[])
|
| 879 |
+
|
| 880 |
+
# 过滤用户交易
|
| 881 |
+
user_transactions = [t for t in transactions_db if t.get("account") == account]
|
| 882 |
+
|
| 883 |
+
# 按类型过滤
|
| 884 |
+
if tx_type != "all":
|
| 885 |
+
type_map = {
|
| 886 |
+
"deposit": ["deposit_pay", "deposit_receive"],
|
| 887 |
+
"final": ["final_pay", "final_receive"],
|
| 888 |
+
"freeze": ["task_freeze", "task_unfreeze"],
|
| 889 |
+
"refund": ["task_refund", "expired_refund"]
|
| 890 |
+
}
|
| 891 |
+
target_types = type_map.get(tx_type, [])
|
| 892 |
+
if target_types:
|
| 893 |
+
user_transactions = [t for t in user_transactions if t.get("type") in target_types]
|
| 894 |
+
|
| 895 |
+
# 分页
|
| 896 |
+
total = len(user_transactions)
|
| 897 |
+
start = (page - 1) * page_size
|
| 898 |
+
end = start + page_size
|
| 899 |
+
paginated = user_transactions[start:end]
|
| 900 |
+
|
| 901 |
+
return {
|
| 902 |
+
"status": "success",
|
| 903 |
+
"data": paginated,
|
| 904 |
+
"total": total,
|
| 905 |
+
"page": page,
|
| 906 |
+
"page_size": page_size
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
# ==========================================
|
| 911 |
+
# 📈 P2增强:获取用户任务收益统计
|
| 912 |
+
# ==========================================
|
| 913 |
+
@router.get("/api/tasks/stats/{account}")
|
| 914 |
+
async def get_task_stats(account: str):
|
| 915 |
+
"""
|
| 916 |
+
获取用户任务统计信息
|
| 917 |
+
"""
|
| 918 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 919 |
+
transactions_db = db.load_data("transactions.json", default_data=[])
|
| 920 |
+
|
| 921 |
+
# 统计任务数量
|
| 922 |
+
published_tasks = [t for t in tasks_db if t.get("publisher") == account]
|
| 923 |
+
assigned_tasks = [t for t in tasks_db if t.get("assignee") == account]
|
| 924 |
+
|
| 925 |
+
# 分状态统计
|
| 926 |
+
published_open = len([t for t in published_tasks if t.get("status") == "open"])
|
| 927 |
+
published_in_progress = len([t for t in published_tasks if t.get("status") in ["assigned", "in_progress", "submitted"]])
|
| 928 |
+
published_completed = len([t for t in published_tasks if t.get("status") == "completed"])
|
| 929 |
+
|
| 930 |
+
assigned_in_progress = len([t for t in assigned_tasks if t.get("status") in ["in_progress", "submitted"]])
|
| 931 |
+
assigned_completed = len([t for t in assigned_tasks if t.get("status") == "completed"])
|
| 932 |
+
|
| 933 |
+
# 统计收入(从交易记录中计算)
|
| 934 |
+
user_transactions = [t for t in transactions_db if t.get("account") == account]
|
| 935 |
+
|
| 936 |
+
# 任务收入(收到的订金+尾款)
|
| 937 |
+
task_income = sum(
|
| 938 |
+
t.get("amount", 0)
|
| 939 |
+
for t in user_transactions
|
| 940 |
+
if t.get("type") in ["deposit_receive", "final_receive"]
|
| 941 |
+
)
|
| 942 |
+
|
| 943 |
+
# 任务支出(支付的订金+尾款)
|
| 944 |
+
task_expense = sum(
|
| 945 |
+
t.get("amount", 0)
|
| 946 |
+
for t in user_transactions
|
| 947 |
+
if t.get("type") in ["deposit_pay", "final_pay"]
|
| 948 |
+
)
|
| 949 |
+
|
| 950 |
+
# 冻结金额(当前招募中的任务)
|
| 951 |
+
frozen_for_tasks = sum(
|
| 952 |
+
t.get("frozenAmount", t.get("totalPrice", 0))
|
| 953 |
+
for t in published_tasks
|
| 954 |
+
if t.get("status") == "open"
|
| 955 |
+
)
|
| 956 |
+
|
| 957 |
+
return {
|
| 958 |
+
"status": "success",
|
| 959 |
+
"data": {
|
| 960 |
+
# 发布任务统计
|
| 961 |
+
"published": {
|
| 962 |
+
"total": len(published_tasks),
|
| 963 |
+
"open": published_open,
|
| 964 |
+
"in_progress": published_in_progress,
|
| 965 |
+
"completed": published_completed
|
| 966 |
+
},
|
| 967 |
+
# 接单任务统计
|
| 968 |
+
"assigned": {
|
| 969 |
+
"total": len(assigned_tasks),
|
| 970 |
+
"in_progress": assigned_in_progress,
|
| 971 |
+
"completed": assigned_completed
|
| 972 |
+
},
|
| 973 |
+
# 财务统计
|
| 974 |
+
"finance": {
|
| 975 |
+
"task_income": task_income, # 任务收入
|
| 976 |
+
"task_expense": task_expense, # 任务支出
|
| 977 |
+
"frozen_for_tasks": frozen_for_tasks # 任务冻结金额
|
| 978 |
+
}
|
| 979 |
+
}
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
# ==========================================
|
| 984 |
+
# ⚖️ P3增强:发起申诉
|
| 985 |
+
# ==========================================
|
| 986 |
+
@router.post("/api/tasks/dispute")
|
| 987 |
+
async def create_dispute(req: TaskDispute):
|
| 988 |
+
"""
|
| 989 |
+
发起任务申诉
|
| 990 |
+
可由发布者或接单者发起
|
| 991 |
+
|
| 992 |
+
触发条件:
|
| 993 |
+
- 验收不通过后,接单者可在 3 天内发起申诉
|
| 994 |
+
- 验收通过后,发布者可在 7 天内发起申诉(发现质量问题)
|
| 995 |
+
"""
|
| 996 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 997 |
+
disputes_db = db.load_data("disputes.json", default_data=[])
|
| 998 |
+
users_db = db.load_data("users.json", default_data={})
|
| 999 |
+
|
| 1000 |
+
# 查找任务
|
| 1001 |
+
task = next((t for t in tasks_db if t.get("id") == req.task_id), None)
|
| 1002 |
+
if not task:
|
| 1003 |
+
raise HTTPException(status_code=404, detail="任务不存在")
|
| 1004 |
+
|
| 1005 |
+
publisher = task.get("publisher")
|
| 1006 |
+
assignee = task.get("assignee")
|
| 1007 |
+
|
| 1008 |
+
# 权限校验:只有发布者或接单者可以发起申诉
|
| 1009 |
+
if req.initiator not in [publisher, assignee]:
|
| 1010 |
+
raise HTTPException(status_code=403, detail="您不是该任务的参与者,无法发起申诉")
|
| 1011 |
+
|
| 1012 |
+
# 状态校验:只有特定状态可以申诉
|
| 1013 |
+
status = task.get("status")
|
| 1014 |
+
|
| 1015 |
+
# 接单者申诉:验收不通过后可申诉
|
| 1016 |
+
if req.initiator == assignee:
|
| 1017 |
+
if status != "in_progress": # 验收不通过后任务回到 in_progress
|
| 1018 |
+
raise HTTPException(status_code=400, detail="当前状态不允许申诉,仅在验收不通过后可发起申诉")
|
| 1019 |
+
|
| 1020 |
+
# 发布者申诉:验收通过后发现问题可申诉
|
| 1021 |
+
if req.initiator == publisher:
|
| 1022 |
+
if status not in ["completed", "in_progress"]:
|
| 1023 |
+
raise HTTPException(status_code=400, detail="当前状态不允许申诉")
|
| 1024 |
+
|
| 1025 |
+
# 检查是否已有未处理的申诉
|
| 1026 |
+
existing_dispute = next(
|
| 1027 |
+
(d for d in disputes_db if d.get("task_id") == req.task_id and d.get("status") in ["pending", "responded"]),
|
| 1028 |
+
None
|
| 1029 |
+
)
|
| 1030 |
+
if existing_dispute:
|
| 1031 |
+
raise HTTPException(status_code=400, detail="该任务已有未处理的申诉")
|
| 1032 |
+
|
| 1033 |
+
# 确定被申诉方
|
| 1034 |
+
respondent = assignee if req.initiator == publisher else publisher
|
| 1035 |
+
respondent_info = users_db.get(respondent, {})
|
| 1036 |
+
initiator_info = users_db.get(req.initiator, {})
|
| 1037 |
+
|
| 1038 |
+
# 创建申诉记录
|
| 1039 |
+
dispute_id = f"dispute_{uuid.uuid4().hex[:12]}"
|
| 1040 |
+
dispute = {
|
| 1041 |
+
"id": dispute_id,
|
| 1042 |
+
"task_id": req.task_id,
|
| 1043 |
+
"task_title": task.get("title", ""),
|
| 1044 |
+
|
| 1045 |
+
# 申诉方信息
|
| 1046 |
+
"initiator": req.initiator,
|
| 1047 |
+
"initiator_name": initiator_info.get("name", req.initiator),
|
| 1048 |
+
"initiator_avatar": initiator_info.get("avatarDataUrl", ""),
|
| 1049 |
+
"initiator_role": "publisher" if req.initiator == publisher else "assignee",
|
| 1050 |
+
"reason": req.reason,
|
| 1051 |
+
"evidence": req.evidence or [],
|
| 1052 |
+
|
| 1053 |
+
# 被申诉方信息
|
| 1054 |
+
"respondent": respondent,
|
| 1055 |
+
"respondent_name": respondent_info.get("name", respondent),
|
| 1056 |
+
"respondent_avatar": respondent_info.get("avatarDataUrl", ""),
|
| 1057 |
+
"response": None,
|
| 1058 |
+
"response_evidence": [],
|
| 1059 |
+
"responded_at": None,
|
| 1060 |
+
|
| 1061 |
+
# 状态与时间
|
| 1062 |
+
"status": "pending", # pending -> responded -> resolved
|
| 1063 |
+
"created_at": int(time.time()),
|
| 1064 |
+
|
| 1065 |
+
# 仲裁信息
|
| 1066 |
+
"admin_account": None,
|
| 1067 |
+
"admin_result": None,
|
| 1068 |
+
"admin_note": None,
|
| 1069 |
+
"resolved_at": None,
|
| 1070 |
+
|
| 1071 |
+
# 涉及金额(争议金额)
|
| 1072 |
+
"disputed_amount": task.get("finalPayment", 0) if status == "in_progress" else task.get("totalPrice", 0)
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
# 更新任务状态为申诉中
|
| 1076 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == req.task_id), None)
|
| 1077 |
+
if task_idx is not None:
|
| 1078 |
+
tasks_db[task_idx]["status"] = "disputed"
|
| 1079 |
+
tasks_db[task_idx]["dispute_id"] = dispute_id
|
| 1080 |
+
|
| 1081 |
+
disputes_db.insert(0, dispute)
|
| 1082 |
+
db.save_data("disputes.json", disputes_db)
|
| 1083 |
+
db.save_data("tasks.json", tasks_db)
|
| 1084 |
+
|
| 1085 |
+
# 发送通知给被申诉方
|
| 1086 |
+
_send_task_notification(
|
| 1087 |
+
account=respondent,
|
| 1088 |
+
title="⚠️ 您有新的任务申诉",
|
| 1089 |
+
content=f"任务『{task.get('title')}』被发起申诉,请尽快回应。申诉理由:{req.reason[:50]}...",
|
| 1090 |
+
task_id=req.task_id
|
| 1091 |
+
)
|
| 1092 |
+
|
| 1093 |
+
# 记录交易
|
| 1094 |
+
_record_transaction(
|
| 1095 |
+
account=req.initiator,
|
| 1096 |
+
tx_type="dispute_initiated",
|
| 1097 |
+
amount=0,
|
| 1098 |
+
related_task_id=req.task_id,
|
| 1099 |
+
target_account=respondent,
|
| 1100 |
+
note=f"发起申诉: {task.get('title', '')[:20]}"
|
| 1101 |
+
)
|
| 1102 |
+
|
| 1103 |
+
return {"status": "success", "data": dispute, "message": "申诉已提交,请等待对方回应"}
|
| 1104 |
+
|
| 1105 |
+
|
| 1106 |
+
# ==========================================
|
| 1107 |
+
# ⚖️ P3增强:被申诉方回应
|
| 1108 |
+
# ==========================================
|
| 1109 |
+
@router.post("/api/tasks/dispute/respond")
|
| 1110 |
+
async def respond_dispute(req: TaskDisputeResponse):
|
| 1111 |
+
"""
|
| 1112 |
+
被申诉方回应申诉
|
| 1113 |
+
"""
|
| 1114 |
+
disputes_db = db.load_data("disputes.json", default_data=[])
|
| 1115 |
+
|
| 1116 |
+
# 查找申诉
|
| 1117 |
+
dispute_idx = next((i for i, d in enumerate(disputes_db) if d.get("id") == req.dispute_id), None)
|
| 1118 |
+
if dispute_idx is None:
|
| 1119 |
+
raise HTTPException(status_code=404, detail="申诉不存在")
|
| 1120 |
+
|
| 1121 |
+
dispute = disputes_db[dispute_idx]
|
| 1122 |
+
|
| 1123 |
+
# 权限校验
|
| 1124 |
+
if dispute.get("respondent") != req.respondent:
|
| 1125 |
+
raise HTTPException(status_code=403, detail="您不是被申诉方,无法回应")
|
| 1126 |
+
|
| 1127 |
+
# 状态校验
|
| 1128 |
+
if dispute.get("status") != "pending":
|
| 1129 |
+
raise HTTPException(status_code=400, detail="该申诉已回应或已处理")
|
| 1130 |
+
|
| 1131 |
+
# 更新申诉记录
|
| 1132 |
+
dispute["response"] = req.response
|
| 1133 |
+
dispute["response_evidence"] = req.evidence or []
|
| 1134 |
+
dispute["responded_at"] = int(time.time())
|
| 1135 |
+
dispute["status"] = "responded"
|
| 1136 |
+
|
| 1137 |
+
disputes_db[dispute_idx] = dispute
|
| 1138 |
+
db.save_data("disputes.json", disputes_db)
|
| 1139 |
+
|
| 1140 |
+
# 发送通知给申诉方
|
| 1141 |
+
_send_task_notification(
|
| 1142 |
+
account=dispute.get("initiator"),
|
| 1143 |
+
title="📝 申诉已收到回应",
|
| 1144 |
+
content=f"您对任务『{dispute.get('task_title')}』的申诉已收到对方回应,等待管理员仲裁。",
|
| 1145 |
+
task_id=dispute.get("task_id")
|
| 1146 |
+
)
|
| 1147 |
+
|
| 1148 |
+
return {"status": "success", "message": "回应已提交,等待管理员仲裁"}
|
| 1149 |
+
|
| 1150 |
+
|
| 1151 |
+
# ==========================================
|
| 1152 |
+
# ⚖️ P3增强:管理员仲裁
|
| 1153 |
+
# ==========================================
|
| 1154 |
+
@router.post("/api/tasks/dispute/resolve")
|
| 1155 |
+
async def resolve_dispute(req: TaskDisputeResolve):
|
| 1156 |
+
"""
|
| 1157 |
+
管理员仲裁申诉
|
| 1158 |
+
|
| 1159 |
+
仲裁结果:
|
| 1160 |
+
- support_initiator: 支持申诉方,争议金额全额给申诉方
|
| 1161 |
+
- support_respondent: 支持被申诉方,争议金额全额给被申诉方
|
| 1162 |
+
- split: 双方分成,按 split_ratio 分配
|
| 1163 |
+
"""
|
| 1164 |
+
# 管理员权限校验
|
| 1165 |
+
if req.admin_account != "123456": # 管理员账号
|
| 1166 |
+
raise HTTPException(status_code=403, detail="无管理员权限")
|
| 1167 |
+
|
| 1168 |
+
disputes_db = db.load_data("disputes.json", default_data=[])
|
| 1169 |
+
tasks_db = db.load_data("tasks.json", default_data=[])
|
| 1170 |
+
users_db = db.load_data("users.json", default_data={})
|
| 1171 |
+
|
| 1172 |
+
# 查找申诉
|
| 1173 |
+
dispute_idx = next((i for i, d in enumerate(disputes_db) if d.get("id") == req.dispute_id), None)
|
| 1174 |
+
if dispute_idx is None:
|
| 1175 |
+
raise HTTPException(status_code=404, detail="申诉不存在")
|
| 1176 |
+
|
| 1177 |
+
dispute = disputes_db[dispute_idx]
|
| 1178 |
+
|
| 1179 |
+
# 状态校验
|
| 1180 |
+
if dispute.get("status") == "resolved":
|
| 1181 |
+
raise HTTPException(status_code=400, detail="该申诉已处理")
|
| 1182 |
+
|
| 1183 |
+
task_id = dispute.get("task_id")
|
| 1184 |
+
task = next((t for t in tasks_db if t.get("id") == task_id), None)
|
| 1185 |
+
if not task:
|
| 1186 |
+
raise HTTPException(status_code=404, detail="关联任务不存在")
|
| 1187 |
+
|
| 1188 |
+
initiator = dispute.get("initiator")
|
| 1189 |
+
respondent = dispute.get("respondent")
|
| 1190 |
+
disputed_amount = dispute.get("disputed_amount", 0)
|
| 1191 |
+
publisher = task.get("publisher")
|
| 1192 |
+
assignee = task.get("assignee")
|
| 1193 |
+
|
| 1194 |
+
# 根据仲裁结果分配资金
|
| 1195 |
+
if req.result == "support_initiator":
|
| 1196 |
+
# 支持申诉方
|
| 1197 |
+
winner = initiator
|
| 1198 |
+
loser = respondent
|
| 1199 |
+
winner_amount = disputed_amount
|
| 1200 |
+
loser_amount = 0
|
| 1201 |
+
result_msg = f"申诉成功,争议金额 {disputed_amount} 积分已分配给申诉方"
|
| 1202 |
+
|
| 1203 |
+
elif req.result == "support_respondent":
|
| 1204 |
+
# 支持被申诉方
|
| 1205 |
+
winner = respondent
|
| 1206 |
+
loser = initiator
|
| 1207 |
+
winner_amount = disputed_amount
|
| 1208 |
+
loser_amount = 0
|
| 1209 |
+
result_msg = f"申诉被驳回,争议金额 {disputed_amount} 积分已分配给被申诉方"
|
| 1210 |
+
|
| 1211 |
+
elif req.result == "split":
|
| 1212 |
+
# 双方分成
|
| 1213 |
+
split_ratio = req.split_ratio or 50
|
| 1214 |
+
winner_amount = int(disputed_amount * split_ratio / 100)
|
| 1215 |
+
loser_amount = disputed_amount - winner_amount
|
| 1216 |
+
result_msg = f"申诉调解成功,申诉方获得 {winner_amount} 积分,被申诉方获得 {loser_amount} 积分"
|
| 1217 |
+
else:
|
| 1218 |
+
raise HTTPException(status_code=400, detail="无效的仲裁结果")
|
| 1219 |
+
|
| 1220 |
+
# 执行资金转移
|
| 1221 |
+
# 如果申诉方是接单者且胜诉,尾款从发布者冻结中扣除并转给接单者
|
| 1222 |
+
# 如果申诉方是发布者且胜诉,需要从接单者账户扣除并退还给发布者
|
| 1223 |
+
|
| 1224 |
+
if req.result == "support_initiator":
|
| 1225 |
+
if dispute.get("initiator_role") == "assignee":
|
| 1226 |
+
# 接单者胜诉:尾款转给接单者
|
| 1227 |
+
_execute_dispute_payment(users_db, publisher, assignee, disputed_amount, task, "assignee_wins")
|
| 1228 |
+
else:
|
| 1229 |
+
# 发布者胜诉:从接单者扣除已支付的订金
|
| 1230 |
+
_execute_dispute_payment(users_db, assignee, publisher, disputed_amount, task, "publisher_wins")
|
| 1231 |
+
elif req.result == "support_respondent":
|
| 1232 |
+
if dispute.get("initiator_role") == "assignee":
|
| 1233 |
+
# 接单者申诉失败:不进行额外资金转移,任务继续
|
| 1234 |
+
pass
|
| 1235 |
+
else:
|
| 1236 |
+
# 发布者申诉失败:尾款继续留给接单者
|
| 1237 |
+
pass
|
| 1238 |
+
elif req.result == "split":
|
| 1239 |
+
# 分成处理
|
| 1240 |
+
_execute_dispute_split(users_db, publisher, assignee, initiator, respondent, winner_amount, loser_amount, task)
|
| 1241 |
+
|
| 1242 |
+
db.save_data("users.json", users_db)
|
| 1243 |
+
|
| 1244 |
+
# 更新申诉状态
|
| 1245 |
+
dispute["status"] = "resolved"
|
| 1246 |
+
dispute["admin_account"] = req.admin_account
|
| 1247 |
+
dispute["admin_result"] = req.result
|
| 1248 |
+
dispute["admin_note"] = req.admin_note
|
| 1249 |
+
dispute["resolved_at"] = int(time.time())
|
| 1250 |
+
if req.result == "split":
|
| 1251 |
+
dispute["split_ratio"] = req.split_ratio
|
| 1252 |
+
|
| 1253 |
+
disputes_db[dispute_idx] = dispute
|
| 1254 |
+
db.save_data("disputes.json", disputes_db)
|
| 1255 |
+
|
| 1256 |
+
# 更新任务状态
|
| 1257 |
+
task_idx = next((i for i, t in enumerate(tasks_db) if t.get("id") == task_id), None)
|
| 1258 |
+
if task_idx is not None:
|
| 1259 |
+
tasks_db[task_idx]["status"] = "resolved" # 申诉已解决
|
| 1260 |
+
tasks_db[task_idx]["dispute_result"] = req.result
|
| 1261 |
+
db.save_data("tasks.json", tasks_db)
|
| 1262 |
+
|
| 1263 |
+
# 发送通知给双方
|
| 1264 |
+
_send_task_notification(
|
| 1265 |
+
account=initiator,
|
| 1266 |
+
title="⚖️ 申诉已处理",
|
| 1267 |
+
content=f"任务『{task.get('title')}』的申诉已由管理员处理。{result_msg}",
|
| 1268 |
+
task_id=task_id
|
| 1269 |
+
)
|
| 1270 |
+
_send_task_notification(
|
| 1271 |
+
account=respondent,
|
| 1272 |
+
title="⚖️ 申诉已处理",
|
| 1273 |
+
content=f"任务『{task.get('title')}』的申诉已由管理员处理。{result_msg}",
|
| 1274 |
+
task_id=task_id
|
| 1275 |
+
)
|
| 1276 |
+
|
| 1277 |
+
# 记录交易
|
| 1278 |
+
_record_transaction(
|
| 1279 |
+
account=req.admin_account,
|
| 1280 |
+
tx_type="dispute_resolved",
|
| 1281 |
+
amount=disputed_amount,
|
| 1282 |
+
related_task_id=task_id,
|
| 1283 |
+
note=f"仲裁申诉: {task.get('title', '')[:20]} - {req.result}"
|
| 1284 |
+
)
|
| 1285 |
+
|
| 1286 |
+
return {"status": "success", "message": result_msg}
|
| 1287 |
+
|
| 1288 |
+
|
| 1289 |
+
def _execute_dispute_payment(users_db: dict, from_account: str, to_account: str, amount: int, task: dict, scenario: str):
|
| 1290 |
+
"""
|
| 1291 |
+
执行申诉仲裁后的资金转移
|
| 1292 |
+
"""
|
| 1293 |
+
from_info = users_db.get(from_account, {})
|
| 1294 |
+
to_info = users_db.get(to_account, {})
|
| 1295 |
+
|
| 1296 |
+
if scenario == "assignee_wins":
|
| 1297 |
+
# 接单者胜诉:从发布者冻结中扣除尾款并转给接单者
|
| 1298 |
+
from_info["balance"] = from_info.get("balance", 0) - amount
|
| 1299 |
+
from_info["frozen_balance"] = max(0, from_info.get("frozen_balance", 0) - amount)
|
| 1300 |
+
to_info["balance"] = to_info.get("balance", 0) + amount
|
| 1301 |
+
|
| 1302 |
+
_record_transaction(from_account, "dispute_loss", amount, task.get("id"), to_account, "申诉裁决扣款")
|
| 1303 |
+
_record_transaction(to_account, "dispute_win", amount, task.get("id"), from_account, "申诉裁决获得")
|
| 1304 |
+
|
| 1305 |
+
elif scenario == "publisher_wins":
|
| 1306 |
+
# 发布者胜诉:从接单者扣除已收到的订金并退给发布者
|
| 1307 |
+
deposit_amount = task.get("depositAmount", 0)
|
| 1308 |
+
from_info["balance"] = max(0, from_info.get("balance", 0) - deposit_amount)
|
| 1309 |
+
to_info["balance"] = to_info.get("balance", 0) + deposit_amount
|
| 1310 |
+
|
| 1311 |
+
_record_transaction(from_account, "dispute_loss", deposit_amount, task.get("id"), to_account, "申诉裁决退还订金")
|
| 1312 |
+
_record_transaction(to_account, "dispute_win", deposit_amount, task.get("id"), from_account, "申诉裁决收回订金")
|
| 1313 |
+
|
| 1314 |
+
users_db[from_account] = from_info
|
| 1315 |
+
users_db[to_account] = to_info
|
| 1316 |
+
|
| 1317 |
+
|
| 1318 |
+
def _execute_dispute_split(users_db: dict, publisher: str, assignee: str, initiator: str, respondent: str,
|
| 1319 |
+
initiator_amount: int, respondent_amount: int, task: dict):
|
| 1320 |
+
"""
|
| 1321 |
+
执行申诉分成处理
|
| 1322 |
+
"""
|
| 1323 |
+
# 从发布者冻结中扣除尾款
|
| 1324 |
+
publisher_info = users_db.get(publisher, {})
|
| 1325 |
+
final_payment = task.get("finalPayment", 0)
|
| 1326 |
+
|
| 1327 |
+
publisher_info["balance"] = publisher_info.get("balance", 0) - final_payment
|
| 1328 |
+
publisher_info["frozen_balance"] = max(0, publisher_info.get("frozen_balance", 0) - final_payment)
|
| 1329 |
+
users_db[publisher] = publisher_info
|
| 1330 |
+
|
| 1331 |
+
# 分配给双方
|
| 1332 |
+
initiator_info = users_db.get(initiator, {})
|
| 1333 |
+
respondent_info = users_db.get(respondent, {})
|
| 1334 |
+
|
| 1335 |
+
initiator_info["balance"] = initiator_info.get("balance", 0) + initiator_amount
|
| 1336 |
+
respondent_info["balance"] = respondent_info.get("balance", 0) + respondent_amount
|
| 1337 |
+
|
| 1338 |
+
users_db[initiator] = initiator_info
|
| 1339 |
+
users_db[respondent] = respondent_info
|
| 1340 |
+
|
| 1341 |
+
_record_transaction(initiator, "dispute_split", initiator_amount, task.get("id"), note="申诉调解分成")
|
| 1342 |
+
_record_transaction(respondent, "dispute_split", respondent_amount, task.get("id"), note="申诉调解分成")
|
| 1343 |
+
|
| 1344 |
+
|
| 1345 |
+
# ==========================================
|
| 1346 |
+
# ⚖️ P3增强:获取申诉列表
|
| 1347 |
+
# ==========================================
|
| 1348 |
+
@router.get("/api/tasks/disputes")
|
| 1349 |
+
async def get_disputes(status: str = "all", page: int = 1, page_size: int = 20):
|
| 1350 |
+
"""
|
| 1351 |
+
获取申诉列表(管理员用)
|
| 1352 |
+
"""
|
| 1353 |
+
disputes_db = db.load_data("disputes.json", default_data=[])
|
| 1354 |
+
|
| 1355 |
+
# 过滤
|
| 1356 |
+
if status != "all":
|
| 1357 |
+
disputes_db = [d for d in disputes_db if d.get("status") == status]
|
| 1358 |
+
|
| 1359 |
+
# 排序:未处理的优先
|
| 1360 |
+
disputes_db.sort(key=lambda x: (0 if x.get("status") in ["pending", "responded"] else 1, -x.get("created_at", 0)))
|
| 1361 |
+
|
| 1362 |
+
# 分页
|
| 1363 |
+
total = len(disputes_db)
|
| 1364 |
+
start = (page - 1) * page_size
|
| 1365 |
+
end = start + page_size
|
| 1366 |
+
paginated = disputes_db[start:end]
|
| 1367 |
+
|
| 1368 |
+
return {
|
| 1369 |
+
"status": "success",
|
| 1370 |
+
"data": paginated,
|
| 1371 |
+
"total": total,
|
| 1372 |
+
"page": page,
|
| 1373 |
+
"page_size": page_size
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
|
| 1377 |
+
# ==========================================
|
| 1378 |
+
# ⚖️ P3增强:获取单个申诉详情
|
| 1379 |
+
# ==========================================
|
| 1380 |
+
@router.get("/api/tasks/disputes/{dispute_id}")
|
| 1381 |
+
async def get_dispute_detail(dispute_id: str):
|
| 1382 |
+
"""
|
| 1383 |
+
获取申诉详情
|
| 1384 |
+
"""
|
| 1385 |
+
disputes_db = db.load_data("disputes.json", default_data=[])
|
| 1386 |
+
|
| 1387 |
+
dispute = next((d for d in disputes_db if d.get("id") == dispute_id), None)
|
| 1388 |
+
if not dispute:
|
| 1389 |
+
raise HTTPException(status_code=404, detail="申诉不存在")
|
| 1390 |
+
|
| 1391 |
+
return {"status": "success", "data": dispute}
|
| 1392 |
+
|
| 1393 |
+
|
| 1394 |
+
# ==========================================
|
| 1395 |
+
# ⚖️ P3增强:获取用户相关的申诉
|
| 1396 |
+
# ==========================================
|
| 1397 |
+
@router.get("/api/tasks/disputes/user/{account}")
|
| 1398 |
+
async def get_user_disputes(account: str):
|
| 1399 |
+
"""
|
| 1400 |
+
获取用户发起或参与的申诉
|
| 1401 |
+
"""
|
| 1402 |
+
disputes_db = db.load_data("disputes.json", default_data=[])
|
| 1403 |
+
|
| 1404 |
+
user_disputes = [
|
| 1405 |
+
d for d in disputes_db
|
| 1406 |
+
if d.get("initiator") == account or d.get("respondent") == account
|
| 1407 |
+
]
|
| 1408 |
+
|
| 1409 |
+
user_disputes.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
| 1410 |
+
|
| 1411 |
+
return {"status": "success", "data": user_disputes}
|