File size: 38,965 Bytes
4fa652c
a0ab3de
4fa652c
 
 
f68778c
 
 
 
25f34c9
4fa652c
856d1b0
2b73d73
 
8f0ec20
088dffd
 
 
25f34c9
 
 
 
 
 
 
08df4e0
 
 
 
 
 
 
 
 
 
 
 
 
 
4fa652c
 
 
 
 
 
 
 
 
 
 
13880c6
8f0ec20
 
 
4fa652c
 
 
 
f68778c
7b74f04
4fa652c
 
 
 
 
8f0ec20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856d1b0
 
8f0ec20
 
 
 
4fa652c
08df4e0
4fa652c
 
 
25f34c9
4fa652c
08df4e0
 
 
 
 
 
c60e0ef
2a653bf
4fa652c
c60e0ef
8f0ec20
e87b2e5
08df4e0
 
 
 
 
4fa652c
088dffd
8fac7d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592cfb6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f37655e
592cfb6
f37655e
 
592cfb6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4fa652c
94dac43
a0ab3de
 
 
8fac7d5
a0ab3de
4fa652c
 
13880c6
a0ab3de
 
 
 
 
 
 
 
 
4fa652c
 
e87b2e5
4fa652c
a0ab3de
 
4fa652c
 
 
 
 
 
8fac7d5
 
 
 
4fa652c
 
7d1c681
ba72d14
f37655e
4fa652c
 
641796b
 
4fa652c
f68778c
4fa652c
 
 
 
 
f68778c
641796b
 
4fa652c
0be9501
4fa652c
71ce6d2
8fac7d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4fa652c
 
7e0f067
 
 
4fa652c
 
 
 
 
 
c60e0ef
 
 
f68778c
c60e0ef
 
8f0ec20
8e0cd3e
cff9a89
 
 
 
856d1b0
 
 
 
 
4fa652c
 
 
8f0ec20
 
bffd56b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4fa652c
 
 
a0ab3de
 
 
 
c60e0ef
5d58797
a0ab3de
4fa652c
 
 
 
5d58797
 
 
 
 
 
 
 
 
 
c60e0ef
5d58797
 
 
 
 
25f34c9
5d58797
25f34c9
 
5d58797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e0cd3e
5d58797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9540e7b
5d58797
9540e7b
5d58797
25f34c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d58797
 
 
 
 
 
4fa652c
f68778c
 
 
 
 
 
 
 
 
4fa652c
f68778c
e87b2e5
f68778c
ea92de7
4fa652c
f68778c
 
 
 
4fa652c
f68778c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1e5e15
 
 
 
 
 
 
 
 
 
 
 
25f34c9
c1e5e15
08df4e0
 
 
 
 
 
 
c1e5e15
2a653bf
c1e5e15
 
 
 
 
 
 
 
f68778c
 
 
 
25f34c9
2b73d73
 
 
 
 
5d58797
2b73d73
5d58797
2b73d73
5d58797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b73d73
5d58797
8f0ec20
 
 
 
 
 
 
 
 
 
 
 
 
cff9a89
 
 
8f0ec20
 
 
9c86741
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f0ec20
 
 
 
856d1b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f0ec20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
# router_items.py
from fastapi import APIRouter, HTTPException, Depends
import time
import uuid
import datetime
import os
import urllib.request
import urllib.error
import json
import asyncio
import 数据库连接 as db
from models import ItemCreate, ItemUpdate, RatingRequest
from 安全认证 import require_auth, check_ownership
from 数据库连接 import invalidate_cache
from db_utils import record_view, sort_cache

router = APIRouter()

def _get_version_str(versions_db: dict, item_id: str) -> str:
    """兼容新旧格式获取版本hash字符串"""
    val = versions_db.get(item_id, "")
    if isinstance(val, dict):
        return val.get("hash", "") or ""
    return val or ""

def _apply_effective_price(item: dict) -> None:
    """检查 pending_price 是否已过生效时间,若是则更新 price 字段供前端显示"""
    pending_price = item.get("pending_price")
    pending_effective = item.get("pending_price_effective_at")
    if pending_price is not None and pending_effective:
        try:
            effective_time = datetime.datetime.fromisoformat(pending_effective)
            if datetime.datetime.now() >= effective_time:
                item["price"] = pending_price
                item["pending_price"] = None
                item["pending_price_effective_at"] = None
        except (ValueError, TypeError):
            pass

def get_last_6_months():
    res = []
    today = datetime.date.today()
    for i in range(5, -1, -1):
        m = today.month - i
        y = today.year
        while m <= 0:
            m += 12
            y -= 1
        res.append(f"{y}-{m:02d}")
    return res

# 数据文件标识,用于排序缓存
data_file = "items.json"

@router.get("/api/items")
async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): # 优化:默认限制调大至 50,提升前端列表体验
    items_db = db.load_data("items.json", default_data=[])
    comments_db = db.load_data("comments.json", default_data={})
    versions_db = db.load_data("versions.json", default_data={}) # 读取版本库
    
    # 如果是推荐榜,匹配所有 recommend 开头的子类型
    if type == "recommend":
        filtered_items = [item for item in items_db if item.get("type", "").startswith("recommend")]
    else:
        filtered_items = [item for item in items_db if item.get("type") == type]
    
    # 🗂️ 使用排序缓存优化排序性能
    cache_key = f"items:{type}:{sort}"
    
    def sort_fn(data):
        if sort == "likes":
            data.sort(key=lambda x: x.get("likes", 0), reverse=True)
        elif sort == "favorites":
            data.sort(key=lambda x: x.get("favorites", 0), reverse=True)
        elif sort == "downloads":
            data.sort(key=lambda x: x.get("uses", 0), reverse=True)
        elif sort == "tips": # 🚀 按近期打赏排序
            current_month = datetime.date.today().strftime("%Y-%m")
            data.sort(key=lambda x: x.get("tip_history", {}).get(current_month, 0), reverse=True)
        elif sort == "views": # 👁️ 按总访问量排序
            data.sort(key=lambda x: x.get("views", 0), reverse=True)
        elif sort == "daily_views": # 👁️ 按日访问量排序
            data.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
        elif sort == "rating": # ⭐ 按评分排序
            data.sort(key=lambda x: (x.get("rating_avg", 0), x.get("rating_count", 0)), reverse=True)
        else: # time 或其他默认
            data.sort(key=lambda x: x.get("created_at", 0), reverse=True)
    
    filtered_items = sort_cache.get_sorted(cache_key, filtered_items, sort_fn)
        
    price_updated = False
    for item in filtered_items:
        item["commentsData"] = comments_db.get(item["id"], [])
        item["comments"] = len(item["commentsData"])
        item["latest_version"] = _get_version_str(versions_db, item["id"])
        
        # 💰 检查延迟价格是否已生效
        old_price = item.get("price", 0)
        _apply_effective_price(item)
        if item.get("price", 0) != old_price:
            price_updated = True
        
        # 🔴 【绝对核心防线】:在下发给前端前,强行在内存中抹除敏感信息!
        item["has_private_token"] = bool(item.get("github_token"))
        item.pop("github_token", None)
        item.pop("netdisk_password", None)  # ☁️ 网盘密码不在列表中显示
        item.pop("viewed_by", None)  # 👁️ 访问者列表不暴露给前端
    
    # 如果有价格生效了,持久化回 items.json
    if price_updated:
        db.save_data("items.json", items_db)
        invalidate_cache("items.json")
    
    return {"status": "success", "data": filtered_items[:limit]}

def _build_creator_trend_data(account: str, u_items: list, months: list) -> dict:
    """

    构建创作者趋势数据

    提取为独立函数供列表接口和详情接口复用

    """
    trend_tools = {m: 0 for m in months}
    trend_apps = {m: 0 for m in months}
    trend_recommends = {m: 0 for m in months}
    
    for i in u_items:
        itype = i.get("type", "")
        history = i.get("use_history", {})
        if itype == "tool" or itype == "recommend_tool": 
            for m in months: trend_tools[m] += history.get(m, 0)
        elif itype == "app" or itype == "recommend_app": 
            for m in months: trend_apps[m] += history.get(m, 0)
        elif itype.startswith("recommend"):
            for m in months: trend_recommends[m] += history.get(m, 0)
    
    return {
        "months": months,
        "tools": [trend_tools[m] for m in months], 
        "apps": [trend_apps[m] for m in months],
        "recommends": [trend_recommends[m] for m in months]
    }


@router.get("/api/creators/search")
async def search_creators(keyword: str, sort: str = "downloads", limit: int = 50):
    """

    搜索创作者

    根据关键词搜索创作者(name、account、shortDesc 不区分大小写子串匹配)

    """
    users_db = db.load_data("users.json", default_data={})
    items_db = db.load_data("items.json", default_data=[])
    
    # 🚀 P1性能优化:预构建 author->items 索引
    author_items_index = {}
    for item in items_db:
        author = item.get("author")
        if author:
            if author not in author_items_index:
                author_items_index[author] = []
            author_items_index[author].append(item)
    
    creators = []
    months = get_last_6_months()
    keyword_lower = keyword.lower()
    
    for account, u in users_db.items():
        # 🚀 P1性能优化:直接从索引获取
        u_items = author_items_index.get(account, [])
        
        # 获取搜索字段并转为小写
        name = u.get("name", account)
        short_desc = u.get("shortDesc") or u.get("intro") or ""
        
        # 不区分大小写的子串匹配(同时覆盖 shortDesc 和 intro 两个字段)
        search_text = f"{name} {account} {short_desc} {u.get('intro') or ''} {u.get('shortDesc') or ''}".lower()
        if keyword_lower not in search_text:
            continue
        
        tools_count = 0
        apps_count = 0
        
        for i in u_items:
            itype = i.get("type", "")
            if itype == "tool": 
                tools_count += 1
            elif itype == "app": 
                apps_count += 1

        creators.append({
            "account": account, "name": name, "avatar": u.get("avatarDataUrl", ""),
            "bannerUrl": u.get("bannerUrl"),
            "shortDesc": short_desc, "fullDesc": short_desc,
            "likes": sum(i.get("likes", 0) for i in u_items), "favorites": sum(i.get("favorites", 0) for i in u_items), 
            "downloads": sum(i.get("uses", 0) for i in u_items), 
            "views": sum(i.get("views", 0) for i in u_items),
            "daily_views": sum(i.get("daily_views", 0) for i in u_items), 
            "toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
            "recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0),
        })
    
    # 排序逻辑与 /api/creators 保持一致
    if sort == "likes": creators.sort(key=lambda x: x.get("likes", 0), reverse=True)
    elif sort == "favorites": creators.sort(key=lambda x: x.get("favorites", 0), reverse=True)
    elif sort == "downloads": creators.sort(key=lambda x: x.get("downloads", 0), reverse=True)
    elif sort == "tips": creators.sort(key=lambda x: x.get("recent_tips", 0), reverse=True)
    elif sort == "views": creators.sort(key=lambda x: x.get("views", 0), reverse=True)
    elif sort == "daily_views": creators.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
    else: creators.sort(key=lambda x: x.get("created_at", 0), reverse=True)
    
    return {"status": "success", "data": creators[:limit]}


@router.get("/api/creators")
async def get_creators(sort: str = "downloads", limit: int = 100):
    """

    获取创作者列表

    🚀 P1性能优化:预构建 author->items 索引,避免 N+1 查询

    ⚡ P2性能优化:移除大字段(trendData、commentsData、tip_board),移至详情接口按需加载

    """
    users_db = db.load_data("users.json", default_data={})
    items_db = db.load_data("items.json", default_data=[])
    
    # 🚀 P1性能优化:预构建 author->items 索引,复杂度从 O(n*m) 降到 O(n+m)
    author_items_index = {}
    for item in items_db:
        author = item.get("author")
        if author:
            if author not in author_items_index:
                author_items_index[author] = []
            author_items_index[author].append(item)
    
    creators = []
    months = get_last_6_months()
    
    for account, u in users_db.items():
        # 🚀 P1性能优化:直接从索引获取,而非遍历全表
        u_items = author_items_index.get(account, [])
        
        tools_count = 0
        apps_count = 0
        
        for i in u_items:
            itype = i.get("type", "")
            if itype == "tool": 
                tools_count += 1
            elif itype == "app": 
                apps_count += 1

        creators.append({
            "account": account, "name": u.get("name", account), "avatar": u.get("avatarDataUrl", ""),
            "bannerUrl": u.get("bannerUrl"),  # 🖼️ 个人资料卡背景图
            "shortDesc": u.get("shortDesc") or u.get("intro") or "", "fullDesc": u.get("shortDesc") or u.get("intro") or "",
            "likes": sum(i.get("likes", 0) for i in u_items), "favorites": sum(i.get("favorites", 0) for i in u_items), 
            "downloads": sum(i.get("uses", 0) for i in u_items), 
            "views": sum(i.get("views", 0) for i in u_items),
            "daily_views": sum(i.get("daily_views", 0) for i in u_items), 
            "toolsCount": tools_count, "appsCount": apps_count, "followers": len(u.get("followers", [])), "created_at": u.get("created_at", 0),
            "recent_tips": u.get("tip_history", {}).get(datetime.date.today().strftime("%Y-%m"), 0), # 🚀 新增:本月收益统计
        })
        
    if sort == "likes": creators.sort(key=lambda x: x.get("likes", 0), reverse=True)
    elif sort == "favorites": creators.sort(key=lambda x: x.get("favorites", 0), reverse=True)
    elif sort == "downloads": creators.sort(key=lambda x: x.get("downloads", 0), reverse=True)
    elif sort == "tips": creators.sort(key=lambda x: x.get("recent_tips", 0), reverse=True) # 🚀 新增:按近期打赏排序
    elif sort == "views": creators.sort(key=lambda x: x.get("views", 0), reverse=True)
    elif sort == "daily_views": creators.sort(key=lambda x: x.get("daily_views", 0), reverse=True)
    else: creators.sort(key=lambda x: x.get("created_at", 0), reverse=True)
    
    return {"status": "success", "data": creators[:limit]}


@router.get("/api/creators/{account}/details")
async def get_creator_details(account: str):
    """

    获取单个创作者的详细数据

    供前端展开卡片时按需加载

    包含:trendData、commentsData、tip_board

    """
    users_db = db.load_data("users.json", default_data={})
    items_db = db.load_data("items.json", default_data=[])
    comments_db = db.load_data("comments.json", default_data={})
    
    # 检查用户是否存在
    if account not in users_db:
        raise HTTPException(status_code=404, detail="创作者不存在")
    
    u = users_db[account]
    
    # 获取该用户的所有作品
    u_items = [item for item in items_db if item.get("author") == account]
    
    # 构建趋势数据
    months = get_last_6_months()
    trend_data = _build_creator_trend_data(account, u_items, months)
    
    return {
        "status": "success",
        "data": {
            "account": account,
            "trendData": trend_data,
            "commentsData": comments_db.get(account, []),
            "tip_board": u.get("tip_board", [])
        }
    }

@router.post("/api/items")
async def create_item(item: ItemCreate):
    if not item.type:
        raise HTTPException(status_code=400, detail="资源类型不能为空")
    item.price = int(item.price or 0)
    if item.price < 0:
        raise HTTPException(status_code=400, detail="🚨 安全拦截:商品价格不能为负数")
        
    items_db = db.load_data("items.json", default_data=[])
    new_item = {
        "id": f"{item.type}_{int(time.time())}_{uuid.uuid4().hex[:6]}", "type": item.type, "title": item.title, "author": item.author,
        "shortDesc": item.shortDesc, "fullDesc": item.fullDesc, "link": item.link, "coverUrl": item.coverUrl, 
        "imageUrls": item.imageUrls or [],  # 🖼️ 效果展示图列表
        "price": item.price, 
        "github_token": item.github_token,
        "netdisk_password": item.netdisk_password,  # ☁️ 网盘密码
        "is_netdisk": item.is_netdisk,              # ☁️ 是否网盘资源
        "is_original": item.is_original,            # 🎨 是否为原创作品
        "allow_refund": item.allow_refund,          # 💸 是否支持退款
        "likes": 0, "favorites": 0, "comments": 0, "uses": 0, "use_history": {}, "created_at": int(time.time()), "liked_by": [], "favorited_by": [],
        "views": 0,
        "daily_views": 0,
        "viewed_by": [],
        "daily_views_date": "",
        "rating_avg": 0.0,
        "rating_count": 0,
        "rating_dist": {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0},
        "rated_by": {}
    }
    items_db.insert(0, new_item)
    db.save_data("items.json", items_db)
    # 🗂️ 清除排序缓存
    sort_cache.invalidate("items:")
    
    # 🚀 首次发布立即触发版本检测与预缓存
    try:
        link = item.link or ""
        if "github.com" in link:
            from 云端_定时版本检测引擎 import fetch_latest_github_hash, precache_github_zip
            
            async def _first_publish_precache():
                try:
                    token = item.github_token or os.environ.get("GITHUB_PAT", "")
                    latest_hash = await fetch_latest_github_hash(link, token)
                    if latest_hash:
                        # 写入 versions.json
                        versions_db = db.load_data("versions.json", default_data={})
                        versions_db[new_item["id"]] = {"hash": latest_hash}
                        db.save_data("versions.json", versions_db)
                        # 触发预缓存
                        await precache_github_zip(link, token, new_item["id"], latest_hash)
                        print(f"[缓存刷新] 首次发布预缓存完成: {new_item['id']}")
                except Exception as e:
                    print(f"[缓存刷新] 首次发布预缓存失败(不影响发布): {e}")
            
            asyncio.create_task(_first_publish_precache())
    except Exception as e:
        print(f"[缓存刷新] 触发首次发布缓存异常(不影响发布): {e}")
    
    return {"status": "success", "data": new_item}

@router.put("/api/items/{item_id}")
async def update_item(item_id: str, update_data: ItemUpdate, current_user: str = Depends(require_auth)):
    """

    更新内容接口

    🔒 P0安全修复:使用 JWT Token 验证用户身份,而非前端传入的 author 参数

    🔄 P7后悔模式:价格修改延迟24小时生效

    🔧 Bug修复:使用 atomic_update 避免并发竞态条件

    """
    if update_data.price is not None:
        update_data.price = int(update_data.price)
        if update_data.price < 0:
            raise HTTPException(status_code=400, detail="🚨 安全拦截:商品价格不能为负数")
    
    result_holder = {}
    
    def updater(items_db):
        for item in items_db:
            if item["id"] == item_id:
                # 🔒 P0安全修复:使用 JWT 解析出的真实用户账号进行校验
                if item.get("author") != current_user:
                    result_holder["error"] = "forbidden"
                    return False
                
                price_change_info = None
                
                if update_data.title is not None: item["title"] = update_data.title
                if update_data.shortDesc is not None: item["shortDesc"] = update_data.shortDesc
                if update_data.fullDesc is not None: item["fullDesc"] = update_data.fullDesc
                old_link = item.get("link", "")
                if update_data.link is not None: item["link"] = update_data.link
                result_holder["link_changed"] = old_link != item.get("link", "")
                result_holder["new_link"] = item.get("link", "")
                if update_data.coverUrl is not None: item["coverUrl"] = update_data.coverUrl
                if update_data.imageUrls is not None: item["imageUrls"] = update_data.imageUrls  # 🖼️ 效果展示图列表
                
                # 🔄 P7后悔模式:价格修改延迟24小时生效
                if update_data.price is not None:
                    current_price = item.get("price", 0)
                    new_price = update_data.price
                    
                    if current_price != new_price:
                        # 设置待生效价格,24小时后生效
                        import datetime
                        effective_time = datetime.datetime.now() + datetime.timedelta(hours=24)
                        item["pending_price"] = new_price
                        item["pending_price_effective_at"] = effective_time.isoformat()
                        price_change_info = {
                            "current_price": current_price,
                            "new_price": new_price,
                            "effective_at": effective_time.isoformat()
                        }
                        # 不立即修改 price,等待24小时后生效
                    else:
                        # 价格未变,清除待生效价格
                        item["pending_price"] = None
                        item["pending_price_effective_at"] = None
                
                if update_data.github_token is not None: item["github_token"] = update_data.github_token 
                if update_data.netdisk_password is not None: item["netdisk_password"] = update_data.netdisk_password  # ☁️
                if update_data.is_netdisk is not None: item["is_netdisk"] = update_data.is_netdisk  # ☁️
                if update_data.is_original is not None: item["is_original"] = update_data.is_original  # 🎨
                if update_data.allow_refund is not None: item["allow_refund"] = update_data.allow_refund  # 💸
                
                result_holder["success"] = True
                result_holder["price_change_info"] = price_change_info
                result_holder["current_price"] = price_change_info.get("current_price") if price_change_info else None
                result_holder["new_price"] = price_change_info.get("new_price") if price_change_info else None
                return True
        
        result_holder["error"] = "not_found"
        return False
    
    db.atomic_update("items.json", updater, default_data=[])
    
    if result_holder.get("error") == "forbidden":
        raise HTTPException(status_code=403, detail="无权修改他人发布的内容")
    if result_holder.get("error") == "not_found":
        raise HTTPException(status_code=404, detail="找不到该内容记录")
    
    # 🗂️ 清除排序缓存和主数据缓存
    sort_cache.invalidate("items:")
    invalidate_cache("items.json")
    
    # 🔄 [缓存刷新] 资源 link 变更时清除 ZIP 缓存元信息
    if result_holder.get("link_changed"):
        try:
            versions_db = db.load_data("versions.json", default_data={})
            entry = versions_db.get(item_id)
            if entry is not None:
                if isinstance(entry, str):
                    entry = {"hash": entry}
                had_meta = "cached_at" in entry or "zip_size" in entry
                entry.pop("cached_at", None)
                entry.pop("zip_size", None)
                if had_meta:
                    versions_db[item_id] = entry
                    db.save_data("versions.json", versions_db)
                    print(f"[缓存刷新] 已清除 item {item_id} 的 ZIP 缓存元信息")
            
            # 可选:触发异步预缓存
            new_link = result_holder.get("new_link", "")
            if new_link and new_link.startswith("https://github.com/"):
                try:
                    from 云端_定时版本检测引擎 import precache_github_zip
                    # 获取当前 item 的 token 和 version_hash
                    items_db = db.load_data("items.json", default_data=[])
                    token = None
                    for it in items_db:
                        if it["id"] == item_id:
                            token = it.get("github_token")
                            break
                    
                    versions_db = db.load_data("versions.json", default_data={})
                    entry = versions_db.get(item_id, {})
                    version_hash = entry.get("hash", "") if isinstance(entry, dict) else entry
                    
                    if version_hash:
                        asyncio.create_task(precache_github_zip(new_link, token, item_id, version_hash))
                        print(f"[缓存刷新] 已触发 item {item_id} 的异步预缓存")
                except ImportError:
                    print(f"[缓存刷新] 预缓存模块导入失败,跳过异步预缓存")
                except Exception as e:
                    print(f"[缓存刷新] 触发异步预缓存失败: {e}")
        except Exception as e:
            print(f"[缓存刷新] 缓存刷新失败(不影响主流程): {e}")
    
    result = {"status": "success"}
    if result_holder.get("price_change_info"):
        price_change_info = result_holder["price_change_info"]
        result["price_change"] = price_change_info
        result["message"] = f"价格将于24小时后从{price_change_info['current_price']}调整为{price_change_info['new_price']}积分"
    return result

# ==========================================
# 🚀 定时任务接口:检查 GitHub 仓库最新版本
# 可通过外部调度器 (cron-job.org / GitHub Actions) 每日 02:00 触发
# ==========================================
@router.post("/api/check_updates")
async def check_github_updates():
    """

    遍历所有 GitHub 类型的工具,获取最新 commit hash,存入 versions.json

    """
    items_db = db.load_data("items.json", default_data=[])
    versions_db = db.load_data("versions.json", default_data={})
    
    updated_count = 0
    
    for item in items_db:
        # 只处理 GitHub 仓库类型的工具
        link = item.get("link", "")
        if not link.startswith("https://github.com/"):
            continue
            
        item_id = item["id"]
        
        try:
            # 解析仓库信息
            repo_parts = link.rstrip("/").replace(".git", "").split("/")
            if len(repo_parts) < 2:
                continue
            owner, repo = repo_parts[-2], repo_parts[-1]
            
            # 获取创作者 Token 或使用全局兜底 Token
            creator_token = item.get("github_token")
            fallback_token = os.environ.get("GITHUB_PAT")
            active_token = creator_token if creator_token else fallback_token
            
            # 请求 GitHub API 获取最新 commit
            api_url = f"https://api.github.com/repos/{owner}/{repo}/commits?per_page=1"
            headers = {
                "Accept": "application/vnd.github.v3+json",
                "User-Agent": "ComfyUI-Ranking-VersionChecker"
            }
            if active_token:
                headers["Authorization"] = f"Bearer {active_token}"
            
            req = urllib.request.Request(api_url, headers=headers)
            with urllib.request.urlopen(req, timeout=10) as response:
                commits = json.loads(response.read().decode("utf-8"))
                if commits and len(commits) > 0:
                    latest_sha = commits[0].get("sha", "")[:7]  # 取前7位作为版本标识
                    
                    # 如果版本有变化,更新 versions.json
                    old_version = versions_db.get(item_id, "")
                    if latest_sha and latest_sha != old_version:
                        versions_db[item_id] = latest_sha
                        updated_count += 1
                        print(f"[版本更新] {item['title']}: {old_version} -> {latest_sha}")
                        
        except urllib.error.HTTPError as e:
            print(f"[版本检查失败] {item.get('title', item_id)}: HTTP {e.code}")
        except Exception as e:
            print(f"[版本检查异常] {item.get('title', item_id)}: {str(e)}")
    
    # 保存更新后的版本库
    db.save_data("versions.json", versions_db)
    
    return {
        "status": "success", 
        "message": f"版本检查完成,共更新 {updated_count} 个工具的版本号",
        "updated_count": updated_count
    }

@router.get("/api/items/{item_id}")
async def get_item_by_id(item_id: str):
    """根据ID获取单个资源的详细信息"""
    items_db = db.load_data("items.json", default_data=[])
    comments_db = db.load_data("comments.json", default_data={})
    versions_db = db.load_data("versions.json", default_data={})
    
    for item in items_db:
        if item["id"] == item_id:
            # 添加关联数据
            item["commentsData"] = comments_db.get(item_id, [])
            item["comments"] = len(item["commentsData"])
            item["latest_version"] = _get_version_str(versions_db, item_id)
            
            # 💰 检查延迟价格是否已生效
            old_price = item.get("price", 0)
            _apply_effective_price(item)
            if item.get("price", 0) != old_price:
                db.save_data("items.json", items_db)
                invalidate_cache("items.json")
            
            # 🔴 【安全防线】:抹除敏感信息
            item["has_private_token"] = bool(item.get("github_token"))
            item.pop("github_token", None)
            item.pop("netdisk_password", None)
            item.pop("viewed_by", None)
            
            return {"status": "success", "data": item}
    
    raise HTTPException(status_code=404, detail="资源不存在")

@router.get("/api/item/{item_id}/version")
async def get_item_version(item_id: str):
    """获取单个资源的最新版本号"""
    versions_db = db.load_data("versions.json", default_data={})
    return {"status": "success", "version": _get_version_str(versions_db, item_id)}

@router.delete("/api/items/{item_id}")
async def delete_item(item_id: str, current_user: str = Depends(require_auth)):
    """

    删除内容(仅作者或管理员可操作)

    🔧 Bug修复:使用 atomic_update 避免并发竞态条件

    """
    result_holder = {}
    
    def items_updater(items_db):
        for i, item in enumerate(items_db):
            if item["id"] == item_id:
                # 🔒 权限检查:仅作者或管理员可删除
                if not check_ownership(item, current_user, owner_field="author", allow_admin=True):
                    result_holder["error"] = "forbidden"
                    return False
                
                # 1. 从 items.json 中删除该条目
                items_db.pop(i)
                result_holder["item_deleted"] = True
                return True
        
        result_holder["error"] = "not_found"
        return False
    
    db.atomic_update("items.json", items_updater, default_data=[])
    
    if result_holder.get("error") == "forbidden":
        raise HTTPException(status_code=403, detail="无权删除他人发布的内容")
    if result_holder.get("error") == "not_found":
        raise HTTPException(status_code=404, detail="找不到该内容记录")
    
    # 2. 清理关联评论:从 comments.json 中删除该内容的所有评论
    def comments_updater(comments_db):
        if item_id in comments_db:
            del comments_db[item_id]
            return True
        return False
    
    db.atomic_update("comments.json", comments_updater, default_data={})
    
    # 3. 清理缓存:使 items.json 和 comments.json 的缓存失效
    invalidate_cache("items.json")
    invalidate_cache("comments.json")
    # 🗂️ 清除排序缓存
    sort_cache.invalidate("items:")
    
    return {"status": "success", "message": "内容已删除"}


@router.post("/api/items/{item_id}/view")
async def record_item_view(item_id: str, current_user: str = Depends(require_auth)):
    """

    记录资源访问量

    👁️ 需要用户认证,每个用户只计算一次总访问量,日访问量每次调用都增加

    """
    result = record_view("items.json", item_id, current_user)
    
    if result is None:
        raise HTTPException(status_code=404, detail="找不到该内容记录")
    
    # 🗂️ 清除排序缓存(浏览量变化可能影响排序)
    sort_cache.invalidate("items:")
    
    return {"status": "success", "views": result["views"], "daily_views": result["daily_views"]}


@router.post("/api/items/{item_id}/use")
async def record_item_use(item_id: str, current_user: str = Depends(require_auth)):
    """

    记录资源使用量/下载量(原子操作,并发安全)

    📥 每个用户对同一资源只计一次,防止重复计数

    """
    result_container = [None]
    current_month = datetime.date.today().strftime("%Y-%m")
    
    def updater(data):
        for item in data:
            if item["id"] == item_id:
                used_by = item.get("used_by", [])
                if current_user in used_by:
                    # 用户已使用过,不重复计数
                    result_container[0] = {"status": "success", "action": "already_used", "uses": item.get("uses", 0)}
                    return
                # 首次使用,增加计数
                used_by.append(current_user)
                item["uses"] = item.get("uses", 0) + 1
                # 更新月度使用历史
                use_history = item.get("use_history", {})
                use_history[current_month] = use_history.get(current_month, 0) + 1
                item["use_history"] = use_history
                item["used_by"] = used_by
                result_container[0] = {"status": "success", "action": "recorded", "uses": item["uses"]}
                return
        result_container[0] = None  # 未找到资源
    
    db.atomic_update("items.json", updater, default_data=[])
    
    if result_container[0] is None:
        raise HTTPException(status_code=404, detail="资源不存在")
    
    # 🗂️ 清除排序缓存(使用数变化可能影响排序)
    sort_cache.invalidate("items:")
    
    return result_container[0]


# ==========================================
# ❤️ 互动接口(点赞/收藏)
# ==========================================

@router.post("/api/items/{item_id}/rating")
async def rate_item(item_id: str, request: RatingRequest, current_user: str = Depends(require_auth)):
    """

    为资源评分(原子操作,并发安全)

    ⭐ score: 1-5

    """
    score = request.score
    if score < 1 or score > 5:
        raise HTTPException(status_code=400, detail="评分必须在1-5之间")
    
    result_container = [None]
    
    def updater(data):
        for item in data:
            if item["id"] == item_id:
                # 禁止自评
                if item.get("author") == current_user:
                    result_container[0] = {"error": "self_rating"}
                    return
                # 初始化评分字段
                if "rating_avg" not in item:
                    item["rating_avg"] = 0.0
                if "rating_count" not in item:
                    item["rating_count"] = 0
                if "rating_dist" not in item:
                    item["rating_dist"] = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0}
                if "rated_by" not in item:
                    item["rated_by"] = {}
                
                rated_by = item["rated_by"]
                rating_dist = item["rating_dist"]
                old_score = None
                if current_user in rated_by:
                    old_score = rated_by[current_user]["score"]
                
                if old_score is not None:
                    # 已评分,先减去旧分数分布
                    rating_dist[str(old_score)] = max(0, rating_dist.get(str(old_score), 0) - 1)
                    rating_dist[str(score)] = rating_dist.get(str(score), 0) + 1
                else:
                    # 未评分,增加计数
                    item["rating_count"] = item.get("rating_count", 0) + 1
                    rating_dist[str(score)] = rating_dist.get(str(score), 0) + 1
                
                rated_by[current_user] = {"score": score, "time": int(time.time())}
                
                # 重新计算平均分
                total = sum(int(k) * v for k, v in rating_dist.items())
                count = item["rating_count"]
                item["rating_avg"] = round(total / count, 2) if count > 0 else 0.0
                
                result_container[0] = {
                    "status": "success",
                    "rating_avg": item["rating_avg"],
                    "rating_count": item["rating_count"],
                    "rating_dist": item["rating_dist"],
                    "user_score": score
                }
                return
        result_container[0] = None  # 未找到资源
    
    db.atomic_update("items.json", updater, default_data=[])
    
    if result_container[0] is None:
        raise HTTPException(status_code=404, detail="资源不存在")
    if result_container[0].get("error") == "self_rating":
        raise HTTPException(status_code=400, detail="不能给自己发布的资源评分")
    
    # 🗂️ 清除排序缓存(评分变化可能影响排序)
    sort_cache.invalidate("items:")
    
    return result_container[0]


@router.post("/api/items/{item_id}/like")
async def toggle_item_like(item_id: str, current_user: str = Depends(require_auth)):
    """

    点赞/取消点赞(原子操作,并发安全)

    """
    result_container = [None]
    
    def updater(data):
        for item in data:
            if item["id"] == item_id:
                liked_by = item.get("liked_by", [])
                if current_user in liked_by:
                    liked_by.remove(current_user)
                    item["likes"] = max(0, item.get("likes", 0) - 1)
                    action = "unliked"
                else:
                    liked_by.append(current_user)
                    item["likes"] = item.get("likes", 0) + 1
                    action = "liked"
                item["liked_by"] = liked_by
                result_container[0] = {"status": "success", "action": action, "likes": item["likes"]}
                return
        result_container[0] = None  # 未找到资源
    
    db.atomic_update("items.json", updater, default_data=[])
    
    if result_container[0] is None:
        raise HTTPException(status_code=404, detail="资源不存在")
    
    # 🗂️ 清除排序缓存(点赞数变化可能影响排序)
    sort_cache.invalidate("items:")
    
    return result_container[0]


@router.post("/api/items/{item_id}/favorite")
async def toggle_item_favorite(item_id: str, current_user: str = Depends(require_auth)):
    """

    收藏/取消收藏(原子操作,并发安全)

    """
    result_container = [None]
    
    def updater(data):
        for item in data:
            if item["id"] == item_id:
                favorited_by = item.get("favorited_by", [])
                if current_user in favorited_by:
                    favorited_by.remove(current_user)
                    item["favorites"] = max(0, item.get("favorites", 0) - 1)
                    action = "unfavorited"
                else:
                    favorited_by.append(current_user)
                    item["favorites"] = item.get("favorites", 0) + 1
                    action = "favorited"
                item["favorited_by"] = favorited_by
                result_container[0] = {"status": "success", "action": action, "favorites": item["favorites"]}
                return
        result_container[0] = None  # 未找到资源
    
    db.atomic_update("items.json", updater, default_data=[])
    
    if result_container[0] is None:
        raise HTTPException(status_code=404, detail="资源不存在")
    
    # 🗂️ 清除排序缓存(收藏数变化可能影响排序)
    sort_cache.invalidate("items:")
    
    return result_container[0]