ZHIWEI666 commited on
Commit
b71f16d
·
verified ·
1 Parent(s): e904a47

Upload 5 files

Browse files
Files changed (4) hide show
  1. app.py +4 -0
  2. models.py +104 -1
  3. router_posts.py +278 -0
  4. 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}