ZHIWEI666 commited on
Commit
8f9d15a
·
verified ·
1 Parent(s): 0bf23ac
app.py CHANGED
@@ -64,7 +64,13 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
64
  import traceback
65
 
66
  limiter = Limiter(key_func=get_remote_address)
67
- app = FastAPI(title="ComfyUI Ranking Community API")
 
 
 
 
 
 
68
  app.state.limiter = limiter
69
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
70
 
 
64
  import traceback
65
 
66
  limiter = Limiter(key_func=get_remote_address)
67
+ app = FastAPI(
68
+ title="ComfyUI Ranking API",
69
+ description="ComfyUI 社区排名系统 API",
70
+ version="1.0.0",
71
+ docs_url="/api/docs",
72
+ redoc_url="/api/redoc"
73
+ )
74
  app.state.limiter = limiter
75
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
76
 
database_sql.py CHANGED
@@ -17,6 +17,10 @@ logger = logging.getLogger("ComfyUI-Ranking.Database")
17
  # 核心:优先读取环境变量中的 PostgreSQL 数据库连接
18
  SQLALCHEMY_DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:////tmp/comfy_financial.db")
19
 
 
 
 
 
20
  # 🚀 P1性能优化:根据数据库类型配置连接池
21
  if "sqlite" in SQLALCHEMY_DATABASE_URL:
22
  # SQLite:使用空连接池,单线程模式
@@ -26,17 +30,19 @@ if "sqlite" in SQLALCHEMY_DATABASE_URL:
26
  connect_args=connect_args,
27
  poolclass=NullPool # SQLite 不需要连接池
28
  )
 
29
  else:
30
  # PostgreSQL/MySQL:配置连接池参数
31
  engine = create_engine(
32
  SQLALCHEMY_DATABASE_URL,
33
  poolclass=QueuePool,
34
- pool_size=5, # 核心连接数
35
- max_overflow=10, # 超出 pool_size 后可创建的最大连接数
36
- pool_timeout=30, # 获取连接超时(秒)
37
- pool_recycle=1800, # 连接回收时间(30分钟),防止数据库断开
38
- pool_pre_ping=True # 使用前检测连接有效性
39
  )
 
40
 
41
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
42
 
 
17
  # 核心:优先读取环境变量中的 PostgreSQL 数据库连接
18
  SQLALCHEMY_DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:////tmp/comfy_financial.db")
19
 
20
+ # 🚀 P2优化:连接池配置支持环境变量
21
+ POOL_SIZE = int(os.environ.get("DB_POOL_SIZE", "5"))
22
+ POOL_OVERFLOW = int(os.environ.get("DB_POOL_OVERFLOW", "10"))
23
+
24
  # 🚀 P1性能优化:根据数据库类型配置连接池
25
  if "sqlite" in SQLALCHEMY_DATABASE_URL:
26
  # SQLite:使用空连接池,单线程模式
 
30
  connect_args=connect_args,
31
  poolclass=NullPool # SQLite 不需要连接池
32
  )
33
+ logger.info(f"数据库引擎初始化完成 (SQLite)")
34
  else:
35
  # PostgreSQL/MySQL:配置连接池参数
36
  engine = create_engine(
37
  SQLALCHEMY_DATABASE_URL,
38
  poolclass=QueuePool,
39
+ pool_size=POOL_SIZE, # 核心连接数(支持环境变量配置)
40
+ max_overflow=POOL_OVERFLOW, # 超出 pool_size 后可创建的最大连接数(支持环境变量配置)
41
+ pool_timeout=30, # 获取连接超时(秒)
42
+ pool_recycle=1800, # 连接回收时间(30分钟),防止数据库断开
43
+ pool_pre_ping=True # 使用前检测连接有效性
44
  )
45
+ logger.info(f"数据库引擎初始化完成 (PostgreSQL/MySQL) - 连接池配置: pool_size={POOL_SIZE}, max_overflow={POOL_OVERFLOW}")
46
 
47
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
48
 
router_posts.py CHANGED
@@ -44,7 +44,7 @@ async def get_posts(page: int = 1, limit: int = 20):
44
  post_data = {
45
  **post,
46
  "author_name": author_info.get("name", post.get("author")),
47
- "author_avatar": author_info.get("avatar", "")
48
  }
49
  result.append(post_data)
50
 
@@ -93,7 +93,7 @@ async def get_post_detail(post_id: str):
93
  "data": {
94
  **post,
95
  "author_name": author_info.get("name", post.get("author")),
96
- "author_avatar": author_info.get("avatar", "")
97
  }
98
  }
99
 
@@ -322,7 +322,7 @@ async def get_post_comments(post_id: str):
322
  result.append({
323
  **c,
324
  "author_name": author_info.get("name", c.get("author")),
325
- "author_avatar": author_info.get("avatar", "")
326
  })
327
 
328
  return {"status": "success", "data": result}
 
44
  post_data = {
45
  **post,
46
  "author_name": author_info.get("name", post.get("author")),
47
+ "author_avatar": author_info.get("avatarDataUrl", "")
48
  }
49
  result.append(post_data)
50
 
 
93
  "data": {
94
  **post,
95
  "author_name": author_info.get("name", post.get("author")),
96
+ "author_avatar": author_info.get("avatarDataUrl", "")
97
  }
98
  }
99
 
 
322
  result.append({
323
  **c,
324
  "author_name": author_info.get("name", c.get("author")),
325
+ "author_avatar": author_info.get("avatarDataUrl", "")
326
  })
327
 
328
  return {"status": "success", "data": result}
router_proxy.py CHANGED
@@ -2,9 +2,11 @@
2
  from fastapi import APIRouter, Depends, HTTPException
3
  from fastapi.responses import StreamingResponse, JSONResponse, Response
4
  from sqlalchemy.orm import Session
5
- from pydantic import BaseModel
 
6
  import httpx
7
  import os
 
8
  import urllib.request
9
  import urllib.error
10
  import 数据库连接 as json_db
@@ -13,10 +15,30 @@ from models_sql import Ownership
13
 
14
  router = APIRouter()
15
 
 
16
  class ProxyGithubZipRequest(BaseModel):
17
- url: str
18
- item_id: str
19
- account: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  @router.post("/api/proxy_github_zip")
22
  async def proxy_github_zip(req_data: ProxyGithubZipRequest, db: Session = Depends(get_db)):
@@ -78,9 +100,36 @@ async def proxy_github_zip(req_data: ProxyGithubZipRequest, db: Session = Depend
78
  # 新增:工作流/应用 (App) JSON 代理下载接口
79
  # ==========================================
80
  class ProxyDownloadRequest(BaseModel):
81
- url: str
82
- item_id: str
83
- account: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  @router.post("/api/proxy_download")
86
  async def proxy_download(req_data: ProxyDownloadRequest, db: Session = Depends(get_db)):
@@ -108,8 +157,13 @@ async def proxy_download(req_data: ProxyDownloadRequest, db: Session = Depends(g
108
  headers["Authorization"] = f"Bearer {hf_token}"
109
 
110
  # 🚀 核心修复:使用异步 httpx 替代同步 urllib,避免阻塞事件循环
 
 
 
 
 
111
  try:
112
- async with httpx.AsyncClient(follow_redirects=True, verify=False, timeout=120.0) as client:
113
  print(f"🔍 开始下载资源 [{req_data.item_id}]")
114
  print(f"🔗 目标地址:{target_url[:80]}...")
115
 
 
2
  from fastapi import APIRouter, Depends, HTTPException
3
  from fastapi.responses import StreamingResponse, JSONResponse, Response
4
  from sqlalchemy.orm import Session
5
+ from pydantic import BaseModel, Field, field_validator, HttpUrl
6
+ from typing import Optional
7
  import httpx
8
  import os
9
+ import re
10
  import urllib.request
11
  import urllib.error
12
  import 数据库连接 as json_db
 
15
 
16
  router = APIRouter()
17
 
18
+
19
  class ProxyGithubZipRequest(BaseModel):
20
+ """GitHub ZIP 代理下载请求模型"""
21
+ url: str = Field(..., min_length=1, description="资源URL")
22
+ item_id: str = Field(..., min_length=1, max_length=100, description="资源ID")
23
+ account: str = Field(..., min_length=1, max_length=50, description="用户账号")
24
+
25
+ @field_validator('item_id')
26
+ @classmethod
27
+ def validate_item_id(cls, v: str) -> str:
28
+ """验证item_id只允许[a-zA-Z0-9_-]字符"""
29
+ if not re.match(r'^[a-zA-Z0-9_-]+$', v):
30
+ raise ValueError('item_id只能包含字母、数字、下划线和连字符')
31
+ return v
32
+
33
+ @field_validator('account')
34
+ @classmethod
35
+ def validate_account(cls, v: str) -> str:
36
+ """验证account非空且长度符合要求"""
37
+ if len(v) < 1:
38
+ raise ValueError('account不能为空')
39
+ if len(v) > 50:
40
+ raise ValueError('account长度不能超过50个字符')
41
+ return v
42
 
43
  @router.post("/api/proxy_github_zip")
44
  async def proxy_github_zip(req_data: ProxyGithubZipRequest, db: Session = Depends(get_db)):
 
100
  # 新增:工作流/应用 (App) JSON 代理下载接口
101
  # ==========================================
102
  class ProxyDownloadRequest(BaseModel):
103
+ """工作流/应用 JSON 代理下载请求模型"""
104
+ url: str = Field(..., min_length=1, description="下载URL")
105
+ item_id: str = Field(..., min_length=1, max_length=100, description="资源ID")
106
+ account: str = Field(..., min_length=1, max_length=50, description="用户账号")
107
+
108
+ @field_validator('item_id')
109
+ @classmethod
110
+ def validate_item_id(cls, v: str) -> str:
111
+ """验证item_id只允许[a-zA-Z0-9_-]字符"""
112
+ if not re.match(r'^[a-zA-Z0-9_-]+$', v):
113
+ raise ValueError('item_id只能包含字母、数字、下划线和连字符')
114
+ return v
115
+
116
+ @field_validator('account')
117
+ @classmethod
118
+ def validate_account(cls, v: str) -> str:
119
+ """验证account非空且长度符合要求"""
120
+ if len(v) < 1:
121
+ raise ValueError('account不能为空')
122
+ if len(v) > 50:
123
+ raise ValueError('account长度不能超过50个字符')
124
+ return v
125
+
126
+ @field_validator('url')
127
+ @classmethod
128
+ def validate_url(cls, v: str) -> str:
129
+ """验证URL格式"""
130
+ if not v.startswith(('http://', 'https://')):
131
+ raise ValueError('url必须是有效的HTTP或HTTPS地址')
132
+ return v
133
 
134
  @router.post("/api/proxy_download")
135
  async def proxy_download(req_data: ProxyDownloadRequest, db: Session = Depends(get_db)):
 
157
  headers["Authorization"] = f"Bearer {hf_token}"
158
 
159
  # 🚀 核心修复:使用异步 httpx 替代同步 urllib,避免阻塞事件循环
160
+ # ⚠️ 安全警告:仅在网络环境导致证书验证失败时,通过环境变量临时关闭
161
+ verify_ssl = os.environ.get("DISABLE_SSL_VERIFY", "").lower() not in ("1", "true")
162
+ if not verify_ssl:
163
+ print("⚠️ SSL证书验证已关闭,请仅在调试环境使用")
164
+
165
  try:
166
+ async with httpx.AsyncClient(follow_redirects=True, verify=verify_ssl, timeout=120.0) as client:
167
  print(f"🔍 开始下载资源 [{req_data.item_id}]")
168
  print(f"🔗 目标地址:{target_url[:80]}...")
169
 
router_tasks.py CHANGED
@@ -90,14 +90,13 @@ def check_and_update_expired_tasks(tasks_db, db_session=None):
90
  - open 状态且超过截止日期:自动取消,退还冻结金额
91
  - in_progress 状态且超过截止日期:标记为过期(不自动取消,需双方处理)
92
  💳 P6支付增强:过期时自动退款
93
- 💳 P0修复:事务完整保护,失败则回滚
94
  """
95
  today = datetime.date.today().isoformat() # "2026-03-30"
96
  updated = False
97
- refund_tasks = [] # 需要退款的任务
98
  expired_task_indices = [] # 记录过期任务的索引
99
 
100
- # 第一阶段:扫描过期任务(不修改数据
101
  for idx, task in enumerate(tasks_db):
102
  deadline = task.get("deadline", "")
103
  status = task.get("status", "")
@@ -124,30 +123,54 @@ def check_and_update_expired_tasks(tasks_db, db_session=None):
124
  task["is_overdue"] = True
125
  updated = True
126
 
127
- # 第二阶段:执行退款操作(事保护)
128
- if expired_task_indices and db_session:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  try:
130
- refund_results = [] # 记录退款结果
131
-
132
- for item in expired_task_indices:
133
- if item["amount"] > 0:
134
- wallet = db_session.query(Wallet).filter(Wallet.account == item["publisher"]).with_for_update().first()
135
- if wallet:
136
- wallet.frozen_balance = max(0, wallet.frozen_balance - item["amount"])
137
- wallet.balance += item["amount"]
138
-
139
- # 记录退款交易
140
- create_task_transaction(
141
- db_session, item["publisher"], "TASK_REFUND",
142
- item["amount"], task_id=item["task_id"]
143
- )
144
-
145
- refund_results.append(item)
146
-
147
- # 💳 P0修复:先 commit 数据库,再修改 JSON
148
- db_session.commit()
149
-
150
- # commit 成功后,修改 JSON 数据
151
  for item in expired_task_indices:
152
  task = tasks_db[item["index"]]
153
  task["status"] = "expired"
@@ -156,33 +179,28 @@ def check_and_update_expired_tasks(tasks_db, db_session=None):
156
  task["refunded"] = True
157
  task["refund_amount"] = item["amount"]
158
 
159
- # 发送退款通知(在事务外执行,失败不影响主流程)
160
- for item in refund_results:
161
- try:
162
- add_notification(item["publisher"], {
163
- "type": "task_refund",
164
- "from_user": "system",
165
- "target_item_id": item["task_id"],
166
- "target_item_title": item["title"],
167
- "content": f"💰 任务《{item['title']}》已过期自动取消,{item['amount']}积分已退还"
168
- })
169
- logger.info(f"TASK_REFUND | publisher={item['publisher']} | task={item['task_id']} | amount={item['amount']}")
170
- except Exception as e:
171
- logger.warning(f"TASK_REFUND_NOTIFY_ERROR | task={item['task_id']} | error={str(e)}")
172
-
173
  except Exception as e:
174
- # 💳 P0修复:事务失败,回滚所有操作
175
- db_session.rollback()
176
- logger.error(f"TASK_EXPIRED_REFUND_ROLLBACK | error={str(e)}")
177
- # 不修改 JSON,下次再试
178
- return False
179
- elif expired_task_indices:
180
- # 没有 db_session 但有过期任务:只更新状态,不处理退款
181
- for item in expired_task_indices:
182
- task = tasks_db[item["index"]]
183
- task["status"] = "expired"
184
- task["expired_at"] = int(time.time())
185
- # 不标记退款,等待下次带 db_session 的调用
 
 
 
 
 
186
 
187
  return updated
188
 
@@ -237,7 +255,7 @@ async def get_tasks(
237
  result.append({
238
  **task,
239
  "publisher_name": publisher_info.get("name", task.get("publisher")),
240
- "publisher_avatar": publisher_info.get("avatar", ""),
241
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
242
  })
243
 
@@ -272,7 +290,7 @@ async def get_task_detail(task_id: str, current_user: str = None):
272
  applicants_with_info.append({
273
  **app,
274
  "name": app_user.get("name", app.get("account")),
275
- "avatar": app_user.get("avatar", "")
276
  })
277
 
278
  return {
@@ -280,9 +298,9 @@ async def get_task_detail(task_id: str, current_user: str = None):
280
  "data": {
281
  **task,
282
  "publisher_name": publisher_info.get("name", task.get("publisher")),
283
- "publisher_avatar": publisher_info.get("avatar", ""),
284
  "assignee_name": assignee_info.get("name") if assignee_info else None,
285
- "assignee_avatar": assignee_info.get("avatar") if assignee_info else None,
286
  "applicants": applicants_with_info,
287
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
288
  }
@@ -732,10 +750,15 @@ async def accept_task(task_id: str, is_accepted: bool, feedback: str = None, cur
732
  task["status"] = "completed"
733
  task["completed_at"] = int(time.time())
734
 
735
- # 💳 P0修复:先保存JSON,再 commit,确保原子性
736
- db.save_data("tasks.json", tasks_db)
737
  db_session.commit()
738
 
 
 
 
 
 
 
739
  logger.info(f"TASK_COMPLETE | publisher={current_user} | assignee={assignee_account} | task={task_id} | total={total_price}")
740
  message = f"验收通过,已支付 {total_price} 积分给接单者"
741
 
@@ -876,9 +899,9 @@ async def get_dispute_detail(dispute_id: str):
876
  "data": {
877
  **dispute,
878
  "publisher_name": publisher_info.get("name", dispute.get("publisher")),
879
- "publisher_avatar": publisher_info.get("avatar", ""),
880
  "assignee_name": assignee_info.get("name", dispute.get("assignee")),
881
- "assignee_avatar": assignee_info.get("avatar", "")
882
  }
883
  }
884
 
 
90
  - open 状态且超过截止日期:自动取消,退还冻结金额
91
  - in_progress 状态且超过截止日期:标记为过期(不自动取消,需双方处理)
92
  💳 P6支付增强:过期时自动退款
93
+ 💳 P0修复:两阶段提交,确保SQL事务与JSON修改原子
94
  """
95
  today = datetime.date.today().isoformat() # "2026-03-30"
96
  updated = False
 
97
  expired_task_indices = [] # 记录过期任务的索引
98
 
99
+ # ========== 第一阶段:预检查(只读,收集需要退款的任务列表 ==========
100
  for idx, task in enumerate(tasks_db):
101
  deadline = task.get("deadline", "")
102
  status = task.get("status", "")
 
123
  task["is_overdue"] = True
124
  updated = True
125
 
126
+ # 如果没有过期任,直接返回
127
+ if not expired_task_indices:
128
+ return updated
129
+
130
+ # 如果没有 db_session,只标记状态,不处理退款
131
+ if not db_session:
132
+ for item in expired_task_indices:
133
+ task = tasks_db[item["index"]]
134
+ task["status"] = "expired"
135
+ task["expired_at"] = int(time.time())
136
+ # 不标记退款,等待下次带 db_session 的调用
137
+ return updated
138
+
139
+ # ========== 第二阶段:执行SQL事务(钱包退款+交易记录) ==========
140
+ refund_results = [] # 记录退款成功的任务
141
+ sql_committed = False
142
+
143
+ try:
144
+ for item in expired_task_indices:
145
+ if item["amount"] > 0:
146
+ wallet = db_session.query(Wallet).filter(Wallet.account == item["publisher"]).with_for_update().first()
147
+ if wallet:
148
+ wallet.frozen_balance = max(0, wallet.frozen_balance - item["amount"])
149
+ wallet.balance += item["amount"]
150
+
151
+ # 记录退款交易
152
+ create_task_transaction(
153
+ db_session, item["publisher"], "TASK_REFUND",
154
+ item["amount"], task_id=item["task_id"]
155
+ )
156
+
157
+ refund_results.append(item)
158
+
159
+ # 💳 P0修复:先 commit SQL 事务
160
+ db_session.commit()
161
+ sql_committed = True
162
+ logger.info(f"TASK_EXPIRED_SQL_COMMIT | refunded_tasks={len(refund_results)}")
163
+
164
+ except Exception as e:
165
+ # 💳 P0修复:SQL事务失败,回滚所有操作,不修改JSON
166
+ db_session.rollback()
167
+ logger.error(f"TASK_EXPIRED_SQL_ROLLBACK | error={str(e)}")
168
+ # 不修改 JSON,下次再试
169
+ return False
170
+
171
+ # ========== 第三阶段:SQL成功后修改JSON任务状态 ==========
172
+ if sql_committed:
173
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  for item in expired_task_indices:
175
  task = tasks_db[item["index"]]
176
  task["status"] = "expired"
 
179
  task["refunded"] = True
180
  task["refund_amount"] = item["amount"]
181
 
182
+ # 💳 P0修复:保存JSON数据
183
+ db.save_data("tasks.json", tasks_db)
184
+ logger.info(f"TASK_EXPIRED_JSON_SAVED | tasks={len(expired_task_indices)}")
185
+
 
 
 
 
 
 
 
 
 
 
186
  except Exception as e:
187
+ # 💳 P0修复:JSON保存失败,记录warning但不回滚SQL(资金已转移,可后续手动恢复)
188
+ logger.warning(f"TASK_EXPIRED_JSON_SAVE_FAILED | error={str(e)} | 资金已退款但任务状态未更新,需手动修复")
189
+ # 不抛出异常,因为SQL事务已经成功,资金已经转移
190
+
191
+ # 发送退款通知(在事务外执行,失败不影响主流程)
192
+ for item in refund_results:
193
+ try:
194
+ add_notification(item["publisher"], {
195
+ "type": "task_refund",
196
+ "from_user": "system",
197
+ "target_item_id": item["task_id"],
198
+ "target_item_title": item["title"],
199
+ "content": f"💰 任务《{item['title']}》已过期自动取消,{item['amount']}积分已退还"
200
+ })
201
+ logger.info(f"TASK_REFUND | publisher={item['publisher']} | task={item['task_id']} | amount={item['amount']}")
202
+ except Exception as e:
203
+ logger.warning(f"TASK_REFUND_NOTIFY_ERROR | task={item['task_id']} | error={str(e)}")
204
 
205
  return updated
206
 
 
255
  result.append({
256
  **task,
257
  "publisher_name": publisher_info.get("name", task.get("publisher")),
258
+ "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
259
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
260
  })
261
 
 
290
  applicants_with_info.append({
291
  **app,
292
  "name": app_user.get("name", app.get("account")),
293
+ "avatar": app_user.get("avatarDataUrl", "")
294
  })
295
 
296
  return {
 
298
  "data": {
299
  **task,
300
  "publisher_name": publisher_info.get("name", task.get("publisher")),
301
+ "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
302
  "assignee_name": assignee_info.get("name") if assignee_info else None,
303
+ "assignee_avatar": assignee_info.get("avatarDataUrl") if assignee_info else None,
304
  "applicants": applicants_with_info,
305
  "status_text": TASK_STATUS.get(task.get("status"), "未知")
306
  }
 
750
  task["status"] = "completed"
751
  task["completed_at"] = int(time.time())
752
 
753
+ # 💳 P0修复:先commit SQL事务(资金为真)再最佳努力写入JSON
 
754
  db_session.commit()
755
 
756
+ # 再最佳努力写入JSON
757
+ try:
758
+ db.save_data("tasks.json", tasks_db)
759
+ except Exception as e:
760
+ logger.warning(f"TASK_COMPLETE_JSON_SAVE_FAILED | 资金已结算但任务状态未更新,需手动修复: {str(e)}")
761
+
762
  logger.info(f"TASK_COMPLETE | publisher={current_user} | assignee={assignee_account} | task={task_id} | total={total_price}")
763
  message = f"验收通过,已支付 {total_price} 积分给接单者"
764
 
 
899
  "data": {
900
  **dispute,
901
  "publisher_name": publisher_info.get("name", dispute.get("publisher")),
902
+ "publisher_avatar": publisher_info.get("avatarDataUrl", ""),
903
  "assignee_name": assignee_info.get("name", dispute.get("assignee")),
904
+ "assignee_avatar": assignee_info.get("avatarDataUrl", "")
905
  }
906
  }
907
 
router_users_auth.py CHANGED
@@ -20,7 +20,7 @@ from models import UserRegister, UserLogin, SendCodeRequest
20
  from verify_code_engine import VERIFY_CODES, send_email_code, send_sms_code, cleanup_expired_codes
21
 
22
  # 🔒 P0安全增强:导入密码哈希和 JWT 工具
23
- from 安全认证 import hash_password, verify_password, create_token
24
 
25
  # 🚀 P2优化:速率限制
26
  from slowapi import Limiter
@@ -237,20 +237,9 @@ async def login_user(request: Request, user: UserLogin):
237
  user_data = users_db[user.account]
238
  stored_password = user_data.get("password", "")
239
 
240
- # 🔒 P0安全增强:密码哈希验证
241
- # 兼容处理:如果存储的是旧版明文密码(非64位哈希),自动升级为哈希
242
- if len(stored_password) != 64:
243
- # 旧版明文密码,直接比对
244
- if stored_password != user.password:
245
- raise HTTPException(status_code=401, detail="密码错误")
246
- # 验证通过后,自动升级为哈希存储
247
- user_data["password"] = hash_password(user.password)
248
- db.save_data("users.json", users_db)
249
- print(f"🔒 自动升级:用户 {user.account} 的密码已升级为哈希存储")
250
- else:
251
- # 新版哈希密码,使用安全验证
252
- if not verify_password(user.password, stored_password):
253
- raise HTTPException(status_code=401, detail="密码错误")
254
 
255
  # 🔒 P0安全增强:生成 JWT Token(替代 mock_token)
256
  token = create_token(user.account)
 
20
  from verify_code_engine import VERIFY_CODES, send_email_code, send_sms_code, cleanup_expired_codes
21
 
22
  # 🔒 P0安全增强:导入密码哈希和 JWT 工具
23
+ from 安全认证 import hash_password, verify_password, create_token, require_password_match
24
 
25
  # 🚀 P2优化:速率限制
26
  from slowapi import Limiter
 
237
  user_data = users_db[user.account]
238
  stored_password = user_data.get("password", "")
239
 
240
+ # 🔒 P0安全增强:密码哈希验证(使用统一验证函数)
241
+ if not require_password_match(stored_password, user.password):
242
+ raise HTTPException(status_code=401, detail="密码错误")
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  # 🔒 P0安全增强:生成 JWT Token(替代 mock_token)
245
  token = create_token(user.account)
router_wallet.py CHANGED
@@ -102,23 +102,32 @@ async def alipay_notify(request: Request, db: Session = Depends(get_db)):
102
  amount = int(float(data.get("total_amount", 0)))
103
  account = data.get("subject", "").split(" - ")[-1]
104
 
105
- wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
106
- if not wallet:
107
- wallet = Wallet(account=account)
108
- db.add(wallet)
109
-
110
- wallet.balance += amount
111
-
112
- last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
113
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
114
- tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
115
-
116
- new_tx = Transaction(
117
- tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
118
- prev_hash=prev_hash, tx_hash=tx_hash
119
- )
120
- db.add(new_tx)
121
- db.commit()
 
 
 
 
 
 
 
 
 
122
 
123
  return Response(content="success", media_type="text/plain")
124
 
@@ -213,46 +222,55 @@ async def purchase_item(request: Request, req: PurchaseRequest, db: Session = De
213
  "netdisk_password": item.get("netdisk_password"), # ☁️
214
  "is_netdisk": item.get("is_netdisk", False) # ☁️
215
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
218
- if not buyer_wallet or buyer_wallet.balance < price:
219
- raise HTTPException(status_code=402, detail="余额不足,请先充值")
220
 
221
- seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
222
- if not seller_wallet:
223
- seller_wallet = Wallet(account=seller_account)
224
- db.add(seller_wallet)
225
 
226
- buyer_wallet.balance -= price
227
- seller_wallet.earn_balance += price
228
-
229
- # 🔄 P7后悔模式:记录购买价格
230
- new_ownership = Ownership(account=req.account, item_id=req.item_id, price_paid=price)
231
- db.add(new_ownership)
232
-
233
- tx_id = f"BUY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
234
- last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
235
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
236
- tx_hash = calculate_tx_hash(tx_id, req.account, "PURCHASE", -price, prev_hash)
237
-
238
- # 创建交易记录 (字段名为 related_account,与 models_sql.py 中 Transaction 模型保持一致)
239
- new_tx = Transaction(
240
- tx_id=tx_id, account=req.account, tx_type="PURCHASE", amount=-price,
241
- related_account=seller_account, item_id=req.item_id, prev_hash=prev_hash, tx_hash=tx_hash
242
- )
243
- db.add(new_tx)
244
- db.commit()
245
-
246
- # 📝 P2优化:购买审计日志
247
- logger.info(f"PURCHASE | buyer={req.account} | seller={seller_account} | item={req.item_id} | amount={price} | tx={tx_id}")
248
-
249
- # ☁️ 购买成功后返回网盘密码
250
- return {
251
- "status": "success",
252
- "already_owned": False,
253
- "netdisk_password": item.get("netdisk_password"), # ☁️ 只有购买成功才返回
254
- "is_netdisk": item.get("is_netdisk", False) # ☁️
255
- }
256
 
257
  @router.post("/api/wallet/tip")
258
  @limiter.limit("20/minute") # 🔒 P0安全优化:打赏每分钟最多20次
@@ -261,83 +279,92 @@ async def tip_user(request: Request, req: TipRequest, db: Session = Depends(get_
261
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
262
  if req.sender_account == req.target_account:
263
  raise HTTPException(status_code=400, detail="不能打赏给自己")
 
 
 
 
 
264
 
265
- sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
266
- target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
267
-
268
- if not sender_wallet or sender_wallet.balance < req.amount:
269
- raise HTTPException(status_code=400, detail="余额不足")
270
- if not target_wallet:
271
- target_wallet = Wallet(account=req.target_account, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
272
- db.add(target_wallet)
273
-
274
- sender_wallet.balance -= req.amount
275
- target_wallet.tip_balance += req.amount
276
-
277
- tx_id_sender = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
278
- tx_id_target = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
279
-
280
- # 记录交易
281
- last_tx_sender = db.query(Transaction).filter(Transaction.account == req.sender_account).order_by(Transaction.created_at.desc()).first()
282
- last_tx_target = db.query(Transaction).filter(Transaction.account == req.target_account).order_by(Transaction.created_at.desc()).first()
283
- prev_hash_sender = last_tx_sender.tx_hash if last_tx_sender else "GENESIS_HASH"
284
- prev_hash_target = last_tx_target.tx_hash if last_tx_target else "GENESIS_HASH"
285
-
286
- # 发送方交易记录 (字段名为 related_account,与 models_sql.py Transaction 模型保持一致)
287
- tx_sender = Transaction(tx_id=tx_id_sender, account=req.sender_account, tx_type="TIP_OUT", amount=-req.amount,
288
- related_account=req.target_account, prev_hash=prev_hash_sender,
289
- tx_hash=calculate_tx_hash(tx_id_sender, req.sender_account, "TIP_OUT", -req.amount, prev_hash_sender))
290
-
291
- # 接收方交易记录
292
- tx_target = Transaction(tx_id=tx_id_target, account=req.target_account, tx_type="TIP_IN", amount=req.amount,
293
- related_account=req.sender_account, prev_hash=prev_hash_target,
294
- tx_hash=calculate_tx_hash(tx_id_target, req.target_account, "TIP_IN", req.amount, prev_hash_target))
295
-
296
- db.add(tx_sender)
297
- db.add(tx_target)
298
- db.commit()
299
-
300
- # 📝 P2优化:打赏审计日志
301
- logger.info(f"TIP | from={req.sender_account} | to={req.target_account} | amount={req.amount} | item={req.item_id or 'N/A'} | anon={req.is_anonymous}")
302
-
303
- # 🚀 核心新增:记录打赏榜单和月度收益趋势 (写入 JSON 以供高频读取)
304
- users_db = json_db.load_data("users.json", default_data={})
305
- items_db = json_db.load_data("items.json", default_data=[])
306
- current_month = datetime.date.today().strftime("%Y-%m")
307
-
308
- # 1. 更新创作者的总打赏榜与收益趋势
309
- if req.target_account in users_db:
310
- u = users_db[req.target_account]
311
- if "tip_history" not in u: u["tip_history"] = {}
312
- u["tip_history"][current_month] = u["tip_history"].get(current_month, 0) + req.amount
313
-
314
- if "tip_board" not in u: u["tip_board"] = []
315
- sender_entry = next((x for x in u["tip_board"] if x["account"] == req.sender_account), None)
316
- if sender_entry:
317
- sender_entry["amount"] += req.amount
318
- else:
319
- u["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
320
- u["tip_board"] = sorted(u["tip_board"], key=lambda x: x["amount"], reverse=True)
321
- json_db.save_data("users.json", users_db)
322
-
323
- # 2. 如果关联了具体作品,更新作品详情的专属打赏榜与收益趋势
324
- if req.item_id:
325
- for item in items_db:
326
- if item["id"] == req.item_id:
327
- if "tip_history" not in item: item["tip_history"] = {}
328
- item["tip_history"][current_month] = item["tip_history"].get(current_month, 0) + req.amount
329
-
330
- if "tip_board" not in item: item["tip_board"] = []
331
- sender_entry = next((x for x in item["tip_board"] if x["account"] == req.sender_account), None)
332
- if sender_entry:
333
- sender_entry["amount"] += req.amount
334
- else:
335
- item["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
336
- item["tip_board"] = sorted(item["tip_board"], key=lambda x: x["amount"], reverse=True)
337
- json_db.save_data("items.json", items_db)
338
- break
339
-
340
- return {"status": "success", "balance": sender_wallet.balance}
 
 
 
 
341
 
342
  @router.post("/api/wallet/withdraw")
343
  @limiter.limit("3/minute") # 🔒 P0安全优化:提现每分钟最多3次
@@ -348,77 +375,86 @@ async def withdraw(request: Request, req: WithdrawRequest, db: Session = Depends
348
  expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
349
  if not code_data or code_data["code"] != req.code or time.time() > expire_time:
350
  raise HTTPException(status_code=400, detail="验证码无效或已过期")
351
-
352
- wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
353
- if not wallet:
354
- raise HTTPException(status_code=400, detail="钱包不存在")
355
 
356
- # 🚀 核心新增阶梯手续费计算 (与前端逻辑统一)
357
- # 查询历史累计提现总额 (WITHDRAW 类型的 amount 是负数,需要取绝对值)
358
- # 🚀 P1性能优化:使用聚合函数代替 .all() + sum
359
- from sqlalchemy import func as sql_func
360
- withdrawals_sum = db.query(sql_func.coalesce(sql_func.sum(Transaction.amount), 0)).filter(
361
- Transaction.account == req.account,
362
- Transaction.tx_type == 'WITHDRAW'
363
- ).scalar() or 0
364
- total_withdrawn = abs(withdrawals_sum)
365
-
366
- # 手续费规则:100元 = 10000积分 免手续费额度,超出部分收取 10%
367
- free_quota = max(0, 10000 - total_withdrawn) # 剩余免责额度
368
- fee_amount = 0
369
- if req.amount > free_quota:
370
- fee_amount = int((req.amount - free_quota) * 0.10) # 只对超出部分收 10%
371
-
372
- actual_withdraw = req.amount # 从账户扣除的金
373
- net_amount = req.amount - fee_amount # 用户实际到账金
374
-
375
- total_withdrawable = wallet.earn_balance + wallet.tip_balance
376
- if actual_withdraw > total_withdrawable:
377
- raise HTTPException(status_code=400, detail="可提现余额不足")
378
-
379
- if actual_withdraw <= wallet.earn_balance:
380
- wallet.earn_balance -= actual_withdraw
381
- else:
382
- remaining = actual_withdraw - wallet.earn_balance
383
- wallet.earn_balance = 0
384
- wallet.tip_balance -= remaining
385
-
386
- wallet.frozen_balance += net_amount # 冻结的是到账金额,非手续费部分
387
-
388
- tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
389
- last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
390
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
391
- tx_hash = calculate_tx_hash(tx_id, req.account, "WITHDRAW", -actual_withdraw, prev_hash)
392
-
393
- new_tx = Transaction(
394
- tx_id=tx_id, account=req.account, tx_type="WITHDRAW", amount=-actual_withdraw,
395
- prev_hash=prev_hash, tx_hash=tx_hash
396
- )
397
- db.add(new_tx)
398
-
399
- # 🚀 如果有手续费,额外记录一笔手续费交易
400
- if fee_amount > 0:
401
- fee_tx_id = f"FEE_{int(time.time())}_{uuid.uuid4().hex[:6]}"
402
- fee_tx_hash = calculate_tx_hash(fee_tx_id, req.account, "WITHDRAW_FEE", -fee_amount, tx_hash)
403
- fee_tx = Transaction(
404
- tx_id=fee_tx_id, account=req.account, tx_type="WITHDRAW_FEE", amount=-fee_amount,
405
- prev_hash=tx_hash, tx_hash=fee_tx_hash
406
  )
407
- db.add(fee_tx)
408
-
409
- db.commit()
410
-
411
- # 📝 P2优化:提现审计日志
412
- logger.info(f"WITHDRAW | account={req.account} | amount={actual_withdraw} | fee={fee_amount} | net={net_amount} | tx={tx_id}")
413
-
414
- del VERIFY_CODES[key]
415
- return {
416
- "status": "success",
417
- "withdraw_amount": actual_withdraw,
418
- "fee_amount": fee_amount,
419
- "net_amount": net_amount,
420
- "free_quota_used": min(req.amount, free_quota + total_withdrawn) - total_withdrawn # 本次消耗的免责额度
421
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  # ==========================================
424
  # 💳 P6支付增强:交易明细查询API
@@ -603,57 +639,66 @@ async def refund_purchase(request: Request, account: str, item_id: str, db: Sess
603
  if refund_amount <= 0:
604
  raise HTTPException(status_code=400, detail="该商品为免费资源,无需退款")
605
 
606
- # 执行退款
607
- buyer_wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
608
- seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
609
-
610
- if seller_wallet:
611
- # 从卖家收益中扣除(如果不足则从余额扣除)
612
- if seller_wallet.earn_balance >= refund_amount:
613
- seller_wallet.earn_balance -= refund_amount
 
 
 
 
 
 
 
 
 
614
  else:
615
- remaining = refund_amount - seller_wallet.earn_balance
616
- seller_wallet.earn_balance = 0
617
- seller_wallet.balance = max(0, seller_wallet.balance - remaining)
618
-
619
- if buyer_wallet:
620
- buyer_wallet.balance += refund_amount
621
- else:
622
- buyer_wallet = Wallet(account=account, balance=refund_amount)
623
- db.add(buyer_wallet)
624
-
625
- # 标记所有权为已退款
626
- ownership.is_refunded = True
627
- ownership.refunded_at = now
628
-
629
- # 创建退款记录(30天禁购)
630
- ban_until = now + datetime.timedelta(days=REFUND_BAN_DAYS)
631
- new_refund = Refund(
632
- account=account,
633
- item_id=item_id,
634
- amount=refund_amount,
635
- ban_until=ban_until
636
- )
637
- db.add(new_refund)
638
-
639
- # 记录退款交易
640
- tx_id = f"REFUND_{int(time.time())}_{uuid.uuid4().hex[:6]}"
641
- last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
642
- prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
643
- tx_hash = calculate_tx_hash(tx_id, account, "REFUND", refund_amount, prev_hash)
644
-
645
- new_tx = Transaction(
646
- tx_id=tx_id, account=account, tx_type="REFUND", amount=refund_amount,
647
- related_account=seller_account, item_id=item_id, prev_hash=prev_hash, tx_hash=tx_hash
648
- )
649
- db.add(new_tx)
650
- db.commit()
651
-
652
- logger.info(f"REFUND | buyer={account} | seller={seller_account} | item={item_id} | amount={refund_amount} | ban_until={ban_until.isoformat()}")
653
-
654
- return {
655
- "status": "success",
656
- "message": f"退款成功,{refund_amount}积分已退还",
657
- "refund_amount": refund_amount,
658
- "ban_days": REFUND_BAN_DAYS
659
- }
 
102
  amount = int(float(data.get("total_amount", 0)))
103
  account = data.get("subject", "").split(" - ")[-1]
104
 
105
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发充值问题
106
+ try:
107
+ wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
108
+ if not wallet:
109
+ wallet = Wallet(account=account)
110
+ db.add(wallet)
111
+
112
+ wallet.balance += amount
113
+
114
+ last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
115
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
116
+ tx_hash = calculate_tx_hash(order_id, account, "RECHARGE", amount, prev_hash)
117
+
118
+ new_tx = Transaction(
119
+ tx_id=order_id, account=account, tx_type="RECHARGE", amount=amount,
120
+ prev_hash=prev_hash, tx_hash=tx_hash
121
+ )
122
+ db.add(new_tx)
123
+ db.commit()
124
+
125
+ # 📝 P2优化:充值审计日志
126
+ logger.info(f"RECHARGE | account={account} | amount={amount} | order={order_id}")
127
+ except Exception as e:
128
+ db.rollback()
129
+ logger.error(f"RECHARGE_ERROR | account={account} | amount={amount} | order={order_id} | error={str(e)}")
130
+ return Response(content="fail", media_type="text/plain")
131
 
132
  return Response(content="success", media_type="text/plain")
133
 
 
222
  "netdisk_password": item.get("netdisk_password"), # ☁️
223
  "is_netdisk": item.get("is_netdisk", False) # ☁️
224
  }
225
+
226
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
227
+ try:
228
+ buyer_wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
229
+ if not buyer_wallet or buyer_wallet.balance < price:
230
+ raise HTTPException(status_code=402, detail="余额不足,请先充值")
231
+
232
+ seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
233
+ if not seller_wallet:
234
+ seller_wallet = Wallet(account=seller_account)
235
+ db.add(seller_wallet)
236
+
237
+ buyer_wallet.balance -= price
238
+ seller_wallet.earn_balance += price
239
 
240
+ # 🔄 P7后悔模式:记录购买价格
241
+ new_ownership = Ownership(account=req.account, item_id=req.item_id, price_paid=price)
242
+ db.add(new_ownership)
243
 
244
+ tx_id = f"BUY_{int(time.time())}_{uuid.uuid4().hex[:6]}"
245
+ last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
246
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
247
+ tx_hash = calculate_tx_hash(tx_id, req.account, "PURCHASE", -price, prev_hash)
248
 
249
+ # 创建交易记录 (字段名为 related_account,与 models_sql.py Transaction 模型保持一致)
250
+ new_tx = Transaction(
251
+ tx_id=tx_id, account=req.account, tx_type="PURCHASE", amount=-price,
252
+ related_account=seller_account, item_id=req.item_id, prev_hash=prev_hash, tx_hash=tx_hash
253
+ )
254
+ db.add(new_tx)
255
+ db.commit()
256
+
257
+ # 📝 P2优化:购买审计日志
258
+ logger.info(f"PURCHASE | buyer={req.account} | seller={seller_account} | item={req.item_id} | amount={price} | tx={tx_id}")
259
+
260
+ # ☁️ 购买成功后返回网盘密码
261
+ return {
262
+ "status": "success",
263
+ "already_owned": False,
264
+ "netdisk_password": item.get("netdisk_password"), # ☁️ 只有购买成功才返回
265
+ "is_netdisk": item.get("is_netdisk", False) # ☁️
266
+ }
267
+ except HTTPException:
268
+ db.rollback()
269
+ raise
270
+ except Exception as e:
271
+ db.rollback()
272
+ logger.error(f"PURCHASE_ERROR | buyer={req.account} | item={req.item_id} | error={str(e)}")
273
+ raise HTTPException(status_code=500, detail="购买处理失败,请稍后重试")
 
 
 
 
 
274
 
275
  @router.post("/api/wallet/tip")
276
  @limiter.limit("20/minute") # 🔒 P0安全优化:打赏每分钟最多20次
 
279
  raise HTTPException(status_code=400, detail="打赏金额必须大于0")
280
  if req.sender_account == req.target_account:
281
  raise HTTPException(status_code=400, detail="不能打赏给自己")
282
+
283
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止双花问题
284
+ try:
285
+ sender_wallet = db.query(Wallet).filter(Wallet.account == req.sender_account).with_for_update().first()
286
+ target_wallet = db.query(Wallet).filter(Wallet.account == req.target_account).with_for_update().first()
287
 
288
+ if not sender_wallet or sender_wallet.balance < req.amount:
289
+ raise HTTPException(status_code=400, detail="余额不足")
290
+ if not target_wallet:
291
+ target_wallet = Wallet(account=req.target_account, balance=0, earn_balance=0, tip_balance=0, frozen_balance=0)
292
+ db.add(target_wallet)
293
+
294
+ sender_wallet.balance -= req.amount
295
+ target_wallet.tip_balance += req.amount
296
+
297
+ tx_id_sender = f"TIP_OUT_{int(time.time())}_{uuid.uuid4().hex[:6]}"
298
+ tx_id_target = f"TIP_IN_{int(time.time())}_{uuid.uuid4().hex[:6]}"
299
+
300
+ # 记录交易
301
+ last_tx_sender = db.query(Transaction).filter(Transaction.account == req.sender_account).order_by(Transaction.created_at.desc()).first()
302
+ last_tx_target = db.query(Transaction).filter(Transaction.account == req.target_account).order_by(Transaction.created_at.desc()).first()
303
+ prev_hash_sender = last_tx_sender.tx_hash if last_tx_sender else "GENESIS_HASH"
304
+ prev_hash_target = last_tx_target.tx_hash if last_tx_target else "GENESIS_HASH"
305
+
306
+ # 发送方交易记录 (字段名为 related_account,与 models_sql.py Transaction 模型保持一致)
307
+ tx_sender = Transaction(tx_id=tx_id_sender, account=req.sender_account, tx_type="TIP_OUT", amount=-req.amount,
308
+ related_account=req.target_account, prev_hash=prev_hash_sender,
309
+ tx_hash=calculate_tx_hash(tx_id_sender, req.sender_account, "TIP_OUT", -req.amount, prev_hash_sender))
310
+
311
+ # 接收方交易记录
312
+ tx_target = Transaction(tx_id=tx_id_target, account=req.target_account, tx_type="TIP_IN", amount=req.amount,
313
+ related_account=req.sender_account, prev_hash=prev_hash_target,
314
+ tx_hash=calculate_tx_hash(tx_id_target, req.target_account, "TIP_IN", req.amount, prev_hash_target))
315
+
316
+ db.add(tx_sender)
317
+ db.add(tx_target)
318
+ db.commit()
319
+
320
+ # 📝 P2优化:打赏审计日志
321
+ logger.info(f"TIP | from={req.sender_account} | to={req.target_account} | amount={req.amount} | item={req.item_id or 'N/A'} | anon={req.is_anonymous}")
322
+
323
+ # 🚀 核心新增记录打赏榜单和月度收益趋势 (写入 JSON 以供高频读取)
324
+ users_db = json_db.load_data("users.json", default_data={})
325
+ items_db = json_db.load_data("items.json", default_data=[])
326
+ current_month = datetime.date.today().strftime("%Y-%m")
327
+
328
+ # 1. 更新创作者的总打赏榜与收益趋势
329
+ if req.target_account in users_db:
330
+ u = users_db[req.target_account]
331
+ if "tip_history" not in u: u["tip_history"] = {}
332
+ u["tip_history"][current_month] = u["tip_history"].get(current_month, 0) + req.amount
333
+
334
+ if "tip_board" not in u: u["tip_board"] = []
335
+ sender_entry = next((x for x in u["tip_board"] if x["account"] == req.sender_account), None)
336
+ if sender_entry:
337
+ sender_entry["amount"] += req.amount
338
+ else:
339
+ u["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
340
+ u["tip_board"] = sorted(u["tip_board"], key=lambda x: x["amount"], reverse=True)
341
+ json_db.save_data("users.json", users_db)
342
+
343
+ # 2. 如果关联了具体作品,更新作品详情的专属打赏榜与收益趋势
344
+ if req.item_id:
345
+ for item in items_db:
346
+ if item["id"] == req.item_id:
347
+ if "tip_history" not in item: item["tip_history"] = {}
348
+ item["tip_history"][current_month] = item["tip_history"].get(current_month, 0) + req.amount
349
+
350
+ if "tip_board" not in item: item["tip_board"] = []
351
+ sender_entry = next((x for x in item["tip_board"] if x["account"] == req.sender_account), None)
352
+ if sender_entry:
353
+ sender_entry["amount"] += req.amount
354
+ else:
355
+ item["tip_board"].append({"account": req.sender_account, "amount": req.amount, "is_anon": req.is_anonymous})
356
+ item["tip_board"] = sorted(item["tip_board"], key=lambda x: x["amount"], reverse=True)
357
+ json_db.save_data("items.json", items_db)
358
+ break
359
+
360
+ return {"status": "success", "balance": sender_wallet.balance}
361
+ except HTTPException:
362
+ db.rollback()
363
+ raise
364
+ except Exception as e:
365
+ db.rollback()
366
+ logger.error(f"TIP_ERROR | from={req.sender_account} | to={req.target_account} | amount={req.amount} | error={str(e)}")
367
+ raise HTTPException(status_code=500, detail="打赏处理失败,请稍后重试")
368
 
369
  @router.post("/api/wallet/withdraw")
370
  @limiter.limit("3/minute") # 🔒 P0安全优化:提现每分钟最多3次
 
375
  expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
376
  if not code_data or code_data["code"] != req.code or time.time() > expire_time:
377
  raise HTTPException(status_code=400, detail="验证码无效或已过期")
 
 
 
 
378
 
379
+ # 🔒 P1并发安全使用悲观锁+异常处理+事务回滚防止并发问题
380
+ try:
381
+ wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
382
+ if not wallet:
383
+ raise HTTPException(status_code=400, detail="钱包不存在")
384
+
385
+ # 🚀 核心新增:阶梯手续费计算 (与前端逻辑统一)
386
+ # 查询历史累计提现总额 (WITHDRAW 类型的 amount 是负数,需要取绝对值)
387
+ # 🚀 P1性能优化:使用聚合函数代替 .all() + sum
388
+ from sqlalchemy import func as sql_func
389
+ withdrawals_sum = db.query(sql_func.coalesce(sql_func.sum(Transaction.amount), 0)).filter(
390
+ Transaction.account == req.account,
391
+ Transaction.tx_type == 'WITHDRAW'
392
+ ).scalar() or 0
393
+ total_withdrawn = abs(withdrawals_sum)
394
+
395
+ # 手续费规则:100元 = 10000积分 免手续费度,超出部分收取 10%
396
+ free_quota = max(0, 10000 - total_withdrawn) # 剩余免责
397
+ fee_amount = 0
398
+ if req.amount > free_quota:
399
+ fee_amount = int((req.amount - free_quota) * 0.10) # 只对超出部分收 10%
400
+
401
+ actual_withdraw = req.amount # 从账户扣除的金额
402
+ net_amount = req.amount - fee_amount # 用户实际到账金额
403
+
404
+ total_withdrawable = wallet.earn_balance + wallet.tip_balance
405
+ if actual_withdraw > total_withdrawable:
406
+ raise HTTPException(status_code=400, detail="可提现余额不足")
407
+
408
+ if actual_withdraw <= wallet.earn_balance:
409
+ wallet.earn_balance -= actual_withdraw
410
+ else:
411
+ remaining = actual_withdraw - wallet.earn_balance
412
+ wallet.earn_balance = 0
413
+ wallet.tip_balance -= remaining
414
+
415
+ wallet.frozen_balance += net_amount # 冻结的是到账金额,非手续费部分
416
+
417
+ tx_id = f"WD_{int(time.time())}_{uuid.uuid4().hex[:6]}"
418
+ last_tx = db.query(Transaction).filter(Transaction.account == req.account).order_by(Transaction.created_at.desc()).first()
419
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
420
+ tx_hash = calculate_tx_hash(tx_id, req.account, "WITHDRAW", -actual_withdraw, prev_hash)
421
+
422
+ new_tx = Transaction(
423
+ tx_id=tx_id, account=req.account, tx_type="WITHDRAW", amount=-actual_withdraw,
424
+ prev_hash=prev_hash, tx_hash=tx_hash
 
 
 
 
425
  )
426
+ db.add(new_tx)
427
+
428
+ # 🚀 如果有手续费,额外记录一笔手续费交易
429
+ if fee_amount > 0:
430
+ fee_tx_id = f"FEE_{int(time.time())}_{uuid.uuid4().hex[:6]}"
431
+ fee_tx_hash = calculate_tx_hash(fee_tx_id, req.account, "WITHDRAW_FEE", -fee_amount, tx_hash)
432
+ fee_tx = Transaction(
433
+ tx_id=fee_tx_id, account=req.account, tx_type="WITHDRAW_FEE", amount=-fee_amount,
434
+ prev_hash=tx_hash, tx_hash=fee_tx_hash
435
+ )
436
+ db.add(fee_tx)
437
+
438
+ db.commit()
439
+
440
+ # 📝 P2优化:提现审计日志
441
+ logger.info(f"WITHDRAW | account={req.account} | amount={actual_withdraw} | fee={fee_amount} | net={net_amount} | tx={tx_id}")
442
+
443
+ del VERIFY_CODES[key]
444
+ return {
445
+ "status": "success",
446
+ "withdraw_amount": actual_withdraw,
447
+ "fee_amount": fee_amount,
448
+ "net_amount": net_amount,
449
+ "free_quota_used": min(req.amount, free_quota + total_withdrawn) - total_withdrawn # 本次消耗的免责额度
450
+ }
451
+ except HTTPException:
452
+ db.rollback()
453
+ raise
454
+ except Exception as e:
455
+ db.rollback()
456
+ logger.error(f"WITHDRAW_ERROR | account={req.account} | amount={req.amount} | error={str(e)}")
457
+ raise HTTPException(status_code=500, detail="提现处理失败,请稍后重试")
458
 
459
  # ==========================================
460
  # 💳 P6支付增强:交易明细查询API
 
639
  if refund_amount <= 0:
640
  raise HTTPException(status_code=400, detail="该商品为免费资源,无需退款")
641
 
642
+ # 🔒 P1并发安全:使用悲观锁+异常处理+事务回滚防止并发问题
643
+ try:
644
+ # 执行退款
645
+ buyer_wallet = db.query(Wallet).filter(Wallet.account == account).with_for_update().first()
646
+ seller_wallet = db.query(Wallet).filter(Wallet.account == seller_account).with_for_update().first()
647
+
648
+ if seller_wallet:
649
+ # 从卖家收益中扣除(如果不足则从余额扣除)
650
+ if seller_wallet.earn_balance >= refund_amount:
651
+ seller_wallet.earn_balance -= refund_amount
652
+ else:
653
+ remaining = refund_amount - seller_wallet.earn_balance
654
+ seller_wallet.earn_balance = 0
655
+ seller_wallet.balance = max(0, seller_wallet.balance - remaining)
656
+
657
+ if buyer_wallet:
658
+ buyer_wallet.balance += refund_amount
659
  else:
660
+ buyer_wallet = Wallet(account=account, balance=refund_amount)
661
+ db.add(buyer_wallet)
662
+
663
+ # 标记所有权为已退款
664
+ ownership.is_refunded = True
665
+ ownership.refunded_at = now
666
+
667
+ # 创建退款记录(30天禁购)
668
+ ban_until = now + datetime.timedelta(days=REFUND_BAN_DAYS)
669
+ new_refund = Refund(
670
+ account=account,
671
+ item_id=item_id,
672
+ amount=refund_amount,
673
+ ban_until=ban_until
674
+ )
675
+ db.add(new_refund)
676
+
677
+ # 记录退款交易
678
+ tx_id = f"REFUND_{int(time.time())}_{uuid.uuid4().hex[:6]}"
679
+ last_tx = db.query(Transaction).filter(Transaction.account == account).order_by(Transaction.created_at.desc()).first()
680
+ prev_hash = last_tx.tx_hash if last_tx else "GENESIS_HASH"
681
+ tx_hash = calculate_tx_hash(tx_id, account, "REFUND", refund_amount, prev_hash)
682
+
683
+ new_tx = Transaction(
684
+ tx_id=tx_id, account=account, tx_type="REFUND", amount=refund_amount,
685
+ related_account=seller_account, item_id=item_id, prev_hash=prev_hash, tx_hash=tx_hash
686
+ )
687
+ db.add(new_tx)
688
+ db.commit()
689
+
690
+ logger.info(f"REFUND | buyer={account} | seller={seller_account} | item={item_id} | amount={refund_amount} | ban_until={ban_until.isoformat()}")
691
+
692
+ return {
693
+ "status": "success",
694
+ "message": f"退款成功,{refund_amount}积分已退还",
695
+ "refund_amount": refund_amount,
696
+ "ban_days": REFUND_BAN_DAYS
697
+ }
698
+ except HTTPException:
699
+ db.rollback()
700
+ raise
701
+ except Exception as e:
702
+ db.rollback()
703
+ logger.error(f"REFUND_ERROR | buyer={account} | item={item_id} | amount={refund_amount} | error={str(e)}")
704
+ raise HTTPException(status_code=500, detail="退款处理失败,请稍后重试")
安全认证.py CHANGED
@@ -74,6 +74,35 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
74
  return hash_password(plain_password) == hashed_password
75
 
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  # ==========================================
78
  # 🎫 JWT Token 生成与验证
79
  # ==========================================
 
74
  return hash_password(plain_password) == hashed_password
75
 
76
 
77
+ def require_password_match(stored_password: str, input_password: str) -> bool:
78
+ """
79
+ 统一的密码验证逻辑
80
+
81
+ 作用:封装密码验证逻辑,统一处理旧版未迁移密码的情况
82
+
83
+ 参数:
84
+ stored_password: 数据库中存储的密码(哈希值)
85
+ input_password: 用户输入的明文密码
86
+
87
+ 返回:
88
+ True 密码匹配
89
+
90
+ 异常:
91
+ HTTPException 401: 旧版未迁移密码,需要重置
92
+
93
+ 使用场景:
94
+ - 用户登录时验证密码
95
+ - 敏感操作前验证当前密码
96
+ """
97
+ if len(stored_password) != 64:
98
+ # 旧版未迁移的密码,拒绝验证并提示重置
99
+ raise HTTPException(
100
+ status_code=401,
101
+ detail="您的账户需要重置密码以完成安全升级,请使用「忘记密码」功能"
102
+ )
103
+ return verify_password(input_password, stored_password)
104
+
105
+
106
  # ==========================================
107
  # 🎫 JWT Token 生成与验证
108
  # ==========================================
数据库连接.py CHANGED
@@ -143,9 +143,16 @@ def invalidate_cache(file_name: str = None):
143
  """
144
  with _cache_lock:
145
  if file_name:
146
- _memory_cache.pop(file_name, None)
 
 
 
 
147
  logger.debug(f"🗑️ 缓存失效: {file_name}")
148
  else:
 
 
 
149
  _memory_cache.clear()
150
  logger.debug("🗑️ 所有缓存已清空")
151
 
 
143
  """
144
  with _cache_lock:
145
  if file_name:
146
+ # 🔒 P0修复:完全清除指定文件的所有缓存数据
147
+ entry = _memory_cache.pop(file_name, None)
148
+ if entry:
149
+ # 清除数据引用,帮助垃圾回收
150
+ entry.clear()
151
  logger.debug(f"🗑️ 缓存失效: {file_name}")
152
  else:
153
+ # 🔒 P0修复:完全清除所有缓存数据
154
+ for entry in _memory_cache.values():
155
+ entry.clear()
156
  _memory_cache.clear()
157
  logger.debug("🗑️ 所有缓存已清空")
158