From 836acf1aa8ed3775542300904082f20b27dc6616 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sat, 11 Jul 2026 01:55:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=8E=E5=8F=B0=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E4=B8=AD=E5=BF=83=E5=92=8C=E4=BA=A7=E5=93=81=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E5=8F=91=E5=B8=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/app.cpython-310.pyc | Bin 0 -> 2828 bytes app.py | 16 +- data/notifications.json | 1 + data/pending_reviews.json | 1 + logs/app.log | 26 +- .../__pycache__/api_cpus.cpython-310.pyc | Bin 5705 -> 6420 bytes .../__pycache__/api_gpus.cpython-310.pyc | Bin 5735 -> 6450 bytes .../__pycache__/api_items.cpython-310.pyc | Bin 5949 -> 6661 bytes .../__pycache__/api_models.cpython-310.pyc | Bin 5622 -> 6376 bytes .../api_notifications.cpython-310.pyc | Bin 0 -> 3792 bytes .../__pycache__/api_reviews.cpython-310.pyc | Bin 0 -> 4633 bytes modules/routes/api_cpus.py | 25 ++ modules/routes/api_gpus.py | 25 ++ modules/routes/api_items.py | 25 ++ modules/routes/api_models.py | 27 ++ modules/routes/api_notifications.py | 109 +++++++ modules/routes/api_reviews.py | 159 +++++++++ templates/admin.html | 307 ++++++++++++++++++ 18 files changed, 703 insertions(+), 18 deletions(-) create mode 100644 __pycache__/app.cpython-310.pyc create mode 100644 data/notifications.json create mode 100644 data/pending_reviews.json create mode 100644 modules/routes/__pycache__/api_notifications.cpython-310.pyc create mode 100644 modules/routes/__pycache__/api_reviews.cpython-310.pyc create mode 100644 modules/routes/api_notifications.py create mode 100644 modules/routes/api_reviews.py diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f51019b7b4fc1153dab2d9f50a8d8f6a3eab5e00 GIT binary patch literal 2828 zcmbVO-BTOa72jR0z83KjnHWp}uQA35gM^8l&MOP_k~?sej1V5aSA?%s2L z=XcM&y5~c(o0}B@9yF!u0o;U|Q0kh9(+erod`5t|9krm=-v}$B_AKnyW*)RUv<~2>*E?~? z2CM}18S{|UrFC(%i=%1tIjdXi=4dxZ512hxuhz@aUXHe#hb>i8IjVB>S@Ve1r}c5P zkD~|8qgKDx&(VI4c9_Sk0d0Vz103x%k6VMi(7o{5_$>7sS z4etLmI>P5CdLP%1^0}YS$M`&e+rYy?bi5*>!9NKfOB@f^rx#v888n1Wpy6u~h|+9wI3D%Savi!3X6CCe&7GYWq)&{w*JUpz3D$%QBSD;jq5;aEPqtL zbFcnzyS{x#X~3pJCPt&@%!0E>;{?C$VaKKM1;?%!^OtDc!H#3t6`CLz83Y%><`Gk$ zw@F5%^4as#GcYGz!@>xgZUGc!CVzH1Kdb*T{}yOXrd>dK(XPxJrCAzt+=6Qqm*Tll z$Vjv~l)$5jFrlqnWt&tBZn@FPPAJl7+4hJ-yQcEfue~&xpPbca@-s6N=O^`7C(h+( zCtk}J*>`#RkwG~N!}&G5Twy^(;st?ZL6VTPD3KJ3%rIP9k05zNB<+DNuSZdILv#Y( zdJM(DGU&!p97!m#2GK;xc?s4fT&UF*aYcj(f}c#2HwD|&t}fTsuBub#XJ&I#Z_MUi z&cB+U&8xdxU)8>Nr?IxY``63$+uQy(x4wJ)+fb-ozvr+1#sB?#jSsKvZf);E%x*ns z+`jAIegC`1S2GbRGiNjc-e@dzw-n2ncFCwrE=6+}41AF`7d*F&D{!*~7b8FmRU_xI zo2#w;?a6-c!6E!Bd{?;mC?>=vBnB+wrYHzYt;|a{@C;X9!Wo4`z(`{jcFQ)RGK&XI z;0kgs8g7}ezDOe_>{11SM%)l(w;nhpagaV0NH+u6>)$;WZL;X(sw~Mz%HG^a^%6L) zz-g=5>Lsca%ec6xvr}{d-D!uH5>X^sOuI;itQTnEFe%l+@V4+3m4O9Lf5SVkoBdppa0K~$8KPsUZI9fz&j3<|FR8u z{lMMb{I>DIroZ;U|9E*^ExT@YJO^o3ESP26amP=-Fna2wHw476jnp?c>NnoqkC-bU z%cyv#f!^5uaCh^rzx8f?{qndP;3vnPR|oyoEr0D3HN46!>o@AoqtAD~-K_ob>x>7# z!$R1;Sl4^JI<9&wQT@lu{*A3ryz;Q|!RJeHXn5>neEOmwoMYv}jtjYJ6PHF2p7TnR znV-?Pt|Pms>ohvgIvs&uav{iW667mOT zfkr*oFdfpwu*d<=k@9Qn-lq8u_#vx7KVKzkmOJ&xJa<8FXBtIWe!6V#X4`f zK~hthr8GIrT%`#v8IFsIK4*HkN{ot2+aa|*6FXUAdoD~t))SS{5B zjy>$GHb~!oR#qsx$LXM&$q8mu_b~pbz{xP9b?rgpHBUwuAsWUpLcGck zeKC}Jm_2bO^zb6|W5KDwOBU2<3ff)@+MgV0krQG}Y7=Ey5mN_XNdMnZq6zWoPbX;Y IlKij#0DXIjssI20 literal 0 HcmV?d00001 diff --git a/app.py b/app.py index ce126c5..8d0583b 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ """ ParamHub - 参数百科 AI大模型与硬件参数速查平台 -v1.8.0 - 模块化重构 + 后台登录认证 +v2.0.0 - 产品审核发布 + 后台通知系统 """ from flask import Flask, request, jsonify, session, redirect, url_for from flask_cors import CORS @@ -10,6 +10,9 @@ from datetime import timedelta from config import SECRET_KEY from utils import load_config +# 审核开关:设为 True 则所有产品需要审核 +REQUIRE_REVIEW = True + # ─── Flask 应用创建 ──────────────────────────────────────────── app = Flask(__name__, static_folder='static', static_url_path='/static') @@ -79,17 +82,24 @@ app.register_blueprint(upload_bp) from modules.routes.api_pin import pin_bp app.register_blueprint(pin_bp) +from modules.routes.api_notifications import notifications_bp +app.register_blueprint(notifications_bp) + +from modules.routes.api_reviews import reviews_bp +app.register_blueprint(reviews_bp) + # ─── 启动 ────────────────────────────────────────────────────── if __name__ == '__main__': print("=" * 50) - print("ParamHub - 参数百科 v1.8.0") + print("ParamHub - 参数百科 v2.0.0") print("=" * 50) - print("模块化重构 + 后台登录认证") + print("产品审核发布 + 后台通知系统") print(f"访问地址: http://localhost:16041") print(f"后台管理: http://localhost:16041/admin") print(f"默认密码: admin123 (可在 config.json 中修改)") + print(f"审核模式: {'开启' if REQUIRE_REVIEW else '关闭'}") print("=" * 50) app.run(host='0.0.0.0', port=16041, debug=False) diff --git a/data/notifications.json b/data/notifications.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/data/notifications.json @@ -0,0 +1 @@ +[] diff --git a/data/pending_reviews.json b/data/pending_reviews.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/data/pending_reviews.json @@ -0,0 +1 @@ +[] diff --git a/logs/app.log b/logs/app.log index 21fe9dd..d95acc7 100644 --- a/logs/app.log +++ b/logs/app.log @@ -1,15 +1,11 @@ -================================================== -ParamHub - 参数百科 v1.8.0 -================================================== -模块化重构 + 后台登录认证 -访问地址: http://localhost:16041 -后台管理: http://localhost:16041/admin -默认密码: admin123 (可在 config.json 中修改) -================================================== - * Serving Flask app 'app' - * Debug mode: off -WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on all addresses (0.0.0.0) - * Running on http://127.0.0.1:16041 - * Running on http://192.168.0.101:16041 -Press CTRL+C to quit +Traceback (most recent call last): + File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-hub-python/app.py", line 52, in + from modules.routes.api_models import models_bp + File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-hub-python/modules/routes/api_models.py", line 18, in + @models_bp.route('/api/models') + File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/scaffold.py", line 43, in wrapper_func + self._check_setup_finished(f_name) + File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/blueprints.py", line 215, in _check_setup_finished + raise AssertionError( +AssertionError: The setup method 'route' can no longer be called on the blueprint 'api_models'. It has already been registered at least once, any changes will not be applied consistently. +Make sure all imports, decorators, functions, etc. needed to set up the blueprint are done before registering it. diff --git a/modules/routes/__pycache__/api_cpus.cpython-310.pyc b/modules/routes/__pycache__/api_cpus.cpython-310.pyc index e568bf6c8a8084428a2d2989b14ddb38457bbfd0..acd4bc707c75ba0f3f14b0aad5e39bb923d1802b 100644 GIT binary patch delta 2817 zcma)7U2GiH6~1?NcXqtH9`B#m_S$I{lMPwOA5nlRS==sl3+LQfAXq_S zNBhma-#Pc(bI(2J%tt$)+h;cV`(p%tm+n43mmRroj?tah@2YiJjK!~z=`>HSWLTn0 zSwD+jA&Yc6s~8iQ0hUzEAWzPL4ol5amcFL349hA$r+AYM_V^qdQv6VlAMWuZEU)-s z#g8gGlW1s+ZBc>|CHMi`+FMXyw<$ia_;I!smS}40+u1fH8D-iODHY{O`dkMO_tlTW zK!^Vk_`BNxo{TffNR=!QkG2WZSZFbHnoJR@p0k97z)tvQ0vGz`iOJ)qj-8maPE0;} zZ1VKclIDl(X48+(a?kcWQKEhf=2or2T2;PEL=slX^V5T13OU(DfJ10V)#lQc}l*gJpI%Cs~D z+^XhlyZ8FEve`(iTy9Vc)lUnH*SgBK~j{ThnEj4Be9C$w#S)edn#Z(I|5I+O2r7gHv43ht9EUM z*%Nole@5=vna8aRu^lxt2w4EXuPK}gw^}X}dy(9OFevxy_wO6S;oSh}pbF2-#hV=5 z2NIOJ8bFMILwWgUeOrHUJl7LWQ|yrc(zh4x1qHPbbV@kB2LXdG;s9G@KKkzx-lq!3 zD)seBvt>CqMZ@na&+iJ}bR#NQ_>B7mpv=?hf7U5i;(S|kzRk(9US@q4eW$;)dWbgr)5=&rB)rG*8%zPtpYjXz#F^2n*ecncGIeC>mth98}` zMct{-`g&07VglsQB>H-#?=as0NBC?*Tr5TWi061!?ni6fb?sRW<;E}Ys+fXBewf*w zeX?~YAsw>z?%V6Xd-mh^UI`re?ET+<^5(l6Z!E#a8#lTe&pxwu{l$&$jZbd8QVKy4 zv?>)aC{BSHaRT7e1i=4O#k0GLwOvJ4C_Z?g`0#<^R8UJ>QB4&TmarH^*bQ*Blo4Tg z`@b-tyIFHCL=Pk5=Y#I%?iP)f$KBnS#=-eq@enE$ZlQ4VCAi{YP*WX7L|`QU+qbvl z>(DrPorykRO1e0WDrhcHJ`N*4(G<{FE1p%iYg}FB=5AuDno>;&OAEvAV#hEvJ(Qpc zYHA6p!4p%Mp!yftsG;R(It2w6JTVYL>2Y}>_QtL7bGs$@K+5MT}@MQ{`Ui)>FSWv^7DZY=@I$YWI+#ZK>jbe ziyn}v)Q*C(sX%q_0n9Y$_4q-C2)LKz+0-6-`JJWI(==X1C!R+5smx`{x(ewLneE?BDfpO#9go)nzrW6^Rhh~j>@0zqPoe1(@oNNilfS~zJV40|vg#@lm_|W< z-G?Lm#BC1NEOHeD6|ARl)CMR;{kT(8hrzxh7EypP@#R0VRUAW>BRq~Uhk)rME+e41 zVB<~UH_AbinZU@nSYTmdgbYZ%VfbfhOoN$WM4^_4V$?KZG;3~!K?43j;yal$>6d8X Bh_wI! delta 2072 zcma)6OK%%h6rMYNP5c-;e&^MuY1DMugqTR7Epbc>2`Y%B4GC=tAj|Qcqz-=R&X_b( z(iBpbsI6|6EbI*^NGwoSNNkXhP!|ZXXcr?`z^*I!37m6mk`S@L*8OzOch9|Zo_BBJ z$*5inh5QQqei~R<|0?lF?`Ol0&R2HKNHfa)Eye@fcVF3M%Q0etz=XI?Ox%pDgAEU_ zF&_C$<=s3=e1iBOk9GJsPY|E%@W~FJ;%VYj#P=NB)64trPva-kw9(H8ItNbhlXRem z_(47ZM^yUsDLzCyyshYa6BzeIOgm@xg~TYJwl$C{Do6fc(buIb1dGO`XG20?V<_ zL=@#?9%$Kep|ZjYxgq($J2@Hy6GIcH&@zY+0&u$OqEs~V4U3EONS;H`2sXngXAm$;A^?z( z&wRfbD-pH4Z#O`!u*UYMUr!I0C`K!ivrtt72fa3z&wpTABN4eJv^->=1 z;HF(DmC;iR6Q!wXC|?B1C}!}i`}(!TC5*0O+P7=mDgt@%!?4`2UV~-r0oY2DwUlke zR_z_vF570dyXk2u>t3#K_Nm&VY$;sLDz>kwwUlNTceNNW?kw<4KlgBNO9kE2(S2Ma z-Ph5(V1RJHtD^^akn{lPtQpu+T8t@n&~|OHT{RA3XbajN8aOonoXUAg{vJq8QW1N> z+og6XvEKxCuwV#Su-6S;U|^26X4?66t5%hFgBjlLLT{m7H>+H9p|uCVP#rff*agb= z@fK?*T9^gf#C*R5_7+w^Q#F=kQAljNG6BkD+rent|E*B7y$}IkSR{6@F850H9a{qhvGZ}#$vby^~`l7uyF*|^*}=s?+Y%0q2ryIeQS1UR{j!7NKbh4?4^U#*t5s|$5DL;`IGXC@W@3NpGL80 zTGkS{ap>of9Jie#zbFgsUY0Bpi@go^(DS3IQAlHuiJNjH^4sbcU!Vddqy3org6Sw3K-G6#1q^t7V z=#MNXKaLH0+hvho#>UvBd>T7DNcRz7>76jnE=)Q;zR*(EnjDCav+Coy_$MszE;>*{ zn3YZ<>wOQ!oa|3#*bO0aSkf1D3k4>OT@9tq0DN&2KaSefrdK;w@fZ=ljr3wW@LsZ8N%!wl(q=q6DR4Mj5 zC?)0hsW`hUpQWaf_kb|^9A7@qYsGwCyom;@awNUHgAcRg5w(VG(gRA5nOHyr>{W3S z;TA#_;T{5gI;@~S#B?A* JV3DNG{st~9nXmu= diff --git a/modules/routes/__pycache__/api_gpus.cpython-310.pyc b/modules/routes/__pycache__/api_gpus.cpython-310.pyc index 2a95b63d89ea15c681ec68556a0730accb9b5887..7d2a976590dd236b58cace3626f7b042ac6d7d86 100644 GIT binary patch delta 2812 zcma)7-ESLJ7N0w|$CI%$@kipsNkiRG)0%!Hbc?n$R_Jz{Lg_9nO-t3NFgU)q-UirrSUT-b%(tTvWFRaTY?fhVmS?lgL6!=vidU|$m`kJ|k?7X(4(xL{9TvVnLEVh)S(Keyo zG<;E6Ak)24V}hoK#-yf?#b&{V#!G}IuBbFgd!;@l^(O7>=u@;`>iawTfsQ^+2c>>M z>W8GCNp!S{4$FzOoVbr}?kvdAEmA)y^&@mMEK%jw_tUL%W{9d&N-isolQ&y75ob^LHVfA0etKm@o5GuzIQsR(ybk8t~djK#t$;`~TyJGtl z%-|A71uztdWk`IcZSD3?=D6Ih^PKobdmyt94CF$vc@X7aBOvcQ0x&H0g}=(-RZ1q7 ziKmt74a>fZ2(K$&VpaBHoloo&6FNP35Uuk7UI1+UsD6Vw|27MC+d>}{^2RPd3==XF zbaepkG6r|bkBVFRmKLTeUb9TsAmpH{ERi-QtGWRGA$8Sqpc0%XL;*Qa4pp>yl_*U@ zgOJQ2s$B`p2b#)Kn1-p|R_B9Fm3C318T^+*BmWLS&2~2f)J!WhNP8Al8oLrAimTH& zO`yI76UoKE0+~|WF4tHf^C35~6m2WB-Bh8ym(;WZ+n9O9HJ5tY%2JH>wXup)dCg!i;ef+K;$k(4-zWu)s*WP~(HeS2YUc36|m1}RWwQt6b~ajhHx4ID?1mFWb$WGfyK5b0_H=!+6w78kld`K9!o#k~)SS#$K64fJJT<~a0YjELg^GUFcP z`Kg3d@goQum`RD6nMyk_<6sH@*k3W!1mP8NU(eT?r1i8&#y%&L;>%b@^RGaV_%4zc zqw(z->2&?xy&g<8==Jz51`3?>VlF;P7H(dTze*yK&5Iby@5J6@UaO-xDb6Ox$h5eg zoO$5qIRzh+pzX4XANYD&EsN3K@z&p9?Hz&+E+ZU3_$$IWfSl>4)n8D7dE_V9BPgX2 zHn3%*jv+|4=23bDAQ$!`c10cr`wqW^6Ud1t?(~jiU&Wz3`xTVr=SvdMS%U=pLyYx} zr{043+$JwE~5 z337Km?>CtVWX9PG5LEPl4!Jh~|6XFKFw*rf6m!2pOx+;8=4KG0@CPNnV=0sT09?O` AssI20 delta 2099 zcma)6OKclO7~WYwcD;Va@4RT5rcIV6X%uK9YBvp`4x+7q&{DObXyte|!HFH4*|pN3 zgh)*}B9Tsyk+3B$IkX3Q<;X+g!VzgDE?iJgMQSAw7x@2KlLV^=67A>z|7PZ&|2bbz z{x%w^g~OTze|!D&D?g@oBYk9O_jGmJj5Xs_YY`fx$~|d^EF=UI0wzo&f=QaO71%?g z%Y?>0muVM`3qB?I5KXlCBuxoE-R9G6K0~vD&j`Nzz)lbCy>|hx%!-9R+TT9#5{9<1Om2H z0F9_5M!bY&AK&-niQWa=zU;{36AoEsY0zBAfaGZTO4+hYwQ7C3hyURnB{4qaTbzmE zTBurFYb+H@c7vHrmsuQ1U!}HgGS&yOP8X>odxf8{?roWxTq8^oO<04Gpq<&tSMEXqcaJdF^& zuUwhTqBI78u8G7fZX9uK99Q78WB?6aN%AdaDC`Di*{ob=L;R6)YTy;vAP&NwlPFIj zV8&PwAjO|6zvv>Bn23Gi_?PPqqkJR}P9VQ*u9|9%;^#4g|Ys;%+8&;)fG&R?w8%{9sbxZ4My%Z`^8?V?C#XYYtSx-Ez4 zJO}$NtdlCMBu(OwN%vnPI40~Gza4%!_8OW#k8n^@CQ2hzQT1t9eG7M7 zjxdXG8DRoI4~jt86(n#NnSy`?6TUnCMjd=N8FnCh2jN`=3{Lkl(If96fsMin2*QVs zMwxW2V1&5G!Uo0&-CofkP7&P=pdJ6E!qvj{0)HA#@e|RFk=%h%Y~9W{aeCiJet>@) z9S*=0HA*GZviR@Op4>qTztAaaR!rMOpD+UP6sa;P(MB+mH<3m~_rEC1qH)}Yxr959q>w-A>g`?t(!%%h-|0dx3ed}P2MHJ572>+ z5#Ho~rt*Ff+zdaTo**~*YI^b1fBO!e5^vcytFF%nt@j0gmdE1-Lsi87 z2}*|vXrI9Ze9E6^a_PIUuJ<~MVbEI1FxXYx{}C@_7q;=8c6_YXfJwmjl+7VrL%4x( z3jwRd>Ii7by|W|pYQ9>djfy#qNwFpYyprlsq2sdfC5a}3rob#@G!jwai@+Ds!XyGD K2qcz{kbeNlRIEh+ diff --git a/modules/routes/__pycache__/api_items.cpython-310.pyc b/modules/routes/__pycache__/api_items.cpython-310.pyc index 16e60a4e8848c35607f75ae5ee55802821390b50..80e0f2f090ebd8babe82dccabe46f5f2e5fcb468 100644 GIT binary patch delta 2912 zcma)8|8E;b7N6O*cQ+f`v12!h)1*y9UE-F67J7WE$%Wn}*9th%Hb<47VC#4`)~0rx zn_Z`*axjp#2NDIUk+^TI&OQ~PmLni4;Dmr5?*5DY;eH|sNJ#v`FLxCZ@6EaiL`A~Z ze0Ju|doyp|ywBVHY~;ehSS1=Y3H<(j;MnZS_RFz8y8H4zyUq;OcAlI{*zrauGnhza$9eVvYLaeKkB&%)ymRfpRw zh8|$#6>88J)!@%NVR-XvI!XJ`QkoN888o5po<+v}9yjr}bf8M#uvJk9c>88I4uU~mYx z(f!a|zHIZmP(On(jBpRaZt-UL(1UW35g1=r_+EJUx?AC%HN6-tk4ND50_=n4DgY+Q zDtt&7+HRT_!`fK-Agah^?!%t6Xop^xxTt-kB~g|Vx6D5AtUkO~>h*%`I`wtOv&)vl zHh1#daz%U2er}$R-Y-7XXEG0>^)C?~LUjQ5It1 zOX^anMjJZQnBG*D!Zj61Rtx`)F!QPk1dG%{ESe^ehOsLO>$nuA#4}i&B~V^Z6V`b} zU8X09*T#}H0%obJ>UOfEd1g&prc1il-iS2GY?KkU?ScXe0Y`JN96Y;n#EUgLnxqkD z-AxLT9+*LA(k$LoV4QA^fpl`*+Zy*VD#!J{mY|>Qkb-^~>y1Q>HYxboS<`CzGMwEr zY80K!?hHr+oJhFU>9XTlGZo%q0SJ=?Zs$GQnyYxuj8n*aPG!zl&)U;4r@56XFW7uP zgu9A#UAuClzW(>Osz{KvH{bu_lebsJi>sesSY2tZHU3q_d|12k3e2qk^?WcnSj8kC z+|cp$vw1$}%oTktAR>Ph=7GR%EnYgzH^2+KSmBG=upjmuuVnjr*>>H0(e}gjlD%M; z_;IlELoDy*C##t0s~>-`_WB!Nd~z*t<(p4m{qnt!*WY~wHeO$8uD@|{_41$Bn=4Zkw-F5K9V^Bgsa1BH>yb5 z?ZF<_FOHO&$R>FRp1uKv$eML4bF!59y+I*zM|h>`+3rYw-f3Nc%O6MmfjcPt@IqYv z1nz-FBh15qVx?_=-PfSV*>jAyfh224r}@*Uh>Hd$PeIR*%yX!O1<#V@V}pDF?Iek1 zaqB?8TIqnaM1s0l5DZ0ABQ!!|N`xx#Hzf{4sCFx*BSpJ_o}EC_;BSHg(3Oc}=C#d| z(La0$y~-iTJ+W|bN%{>J_!!7eqx>;3-ab@6j9nx;|24uC!U2G6yNokGg#sM`D*_)! z!0`B5bsloV=2+wSX@oq243riGN){p+t<8Y^29=~Y(yxP?VF}i=Ff6|ZB{v0NC|H() z@?^sK<*AEeAX2`4Y7rS5!uQ9g5`-Jlk}P*(Dq3 zi;g*9D_>=JCFo9sa!0%t{m*T8Zka#(5*E;>$3!!>f9oY+@=wBB13i^Y$ZB`G0-3Bb z;Cuo#H=` zFtR_Z`5|v{-VSc|Veyan4SHBSnHbPcwkegEC^eJ zKZCMq1k856h;R-d+a{yqw>xEdMy#Fu1)SAdLHT^!o;}Z_KnCSy>}^J-WhEo?7h!fi z+VwgpWVideWwA=ZviLJ-a8^9ib*dhey|65I96ulU5d?-%M8H(#vj|X63CE>^UAJ6{ wkqav0C`QM{Pm&NZR0A@*2U?1n3XBXOUXMv*ZP0c>i^c#z7>k-T-V>wW1Fx`@vj6}9 delta 2127 zcma)6&2JM|5Z||U*6X#sb{v1_JK)3yOhbuC8k`gYDydXHN&*zB%Ch}7mV+Iew;M!| zQ7Q$FJp=b7JoGjHC^ zym>P7{YkwM2xt=gejomDeJ=4-A0k6fj~m-YsNGN14xwJ^c`WUaJ7K|S!1$44y)T|Uy~qckS?nBe0FeiAhKcma=$i-i;&>>3Qw zVPTLE{0JQc6ImQRO2@=XlFBPmcAT%0w)`-`N5hG|KjnGi-j!I4-;?M3TE12_t#YH@ zT#WFG?iaa!xO&!QlLxlzuCZmi?iXt;2Al(NkZjc~t{DYbjq}02XG`PAX=P>}kox-E2t zIZ1@b_k}bE62sKI>QCR!rR%vg9ZN6Ir!UW^SDXTeU~$zc^Y~HRJ&J&h#3ljW%jyg> zVyhLyDy*B04SvZ#{jq2)z3Fwuygu$%m9=*M1I|2yfQU|e}#>Yv^?lyuQtFa&Pk4pbn4 zZ%DZ_utXP~$DO?_irC}$Vy#A{QgdQ2+^?F|Qq8#ic{oXK?WMvKWa^;r#mg7fe&{-N zayqNnZfmP)ICY!h<$)Juf$Nbm#i=ZxiR64De@-QNEt2tx^998D^GK9b_&1T$q{dU> zkv&iJ3qn5P&toZak$)G!M`0{<(P88`@H)tSU@c7xBxOX+|2Eez#euiynm=W_l- zJil#%x-TA|g$tr57)m98?o(t9B(PPT9-c-ijBqHm*O9w|AX2-AQUxIEbzZ5jT5sY6 z7mu}$t4cQs+lk4^yGV#6Y@#G85fZc0vBD8*e#(DIEW}#qG-Z1V1==VV3haF}pnN2G zXT|PgjZJt=IOZ9SRfe6%RuFC?+(M`!pcm)B*YXdXt>)_ux>++8F)`*e|1}vM(TJMR Si0*|i1YZC?)%g!4bn*|!OR*FH diff --git a/modules/routes/__pycache__/api_models.cpython-310.pyc b/modules/routes/__pycache__/api_models.cpython-310.pyc index 125024461e524d200e34342bc65da51e2b9b93e8..f12353825828fb25dfaf1f846f0b02495ee8f149 100644 GIT binary patch delta 2740 zcma)8Yj6}r5Z>9n-P?QQa*vS93lbs)Ib#%1t5A*zYCsfuztvhUH%r)fk1)GO1TP*` zct|NqoYL|UC07E9CBc#(Sd=CH^_TzGUzQ|+U;eT9V`t8lwK=(kR*6AUI** z)X|9G%rj$yphN2i2#ubRX^h4NzfthRG*RO>(0PKNtnnLb{3LA>{3gL~7W?$!MlG~e z2%3eUjn1zvSU}qazeVsn=zLfr3)2f}r;xN#d7G5Fk8dWEl`Nj_?>_>B2IDCh_9B2I zAxfy6lSU+mlqITA->9-n+9nb4?UAStHd0GXpgR5O(dly+rr*CZb@t-z@uRmc9-Er1 zPEUNpaMO)$WNY7+9qYID8C&~yuJ7BmHYK}CW@xC@^$(a%#&KARxIthT`64al%u5oB z@eRsj-Eqq{n0d%DhvDa7$zmprpm%sHvaFK`(cohVm|p#pJWJYG3`h^+K)Jzm&a$0s zF+bGX#>bSm$O7KsYv@j*OwARCO~%lDFz&gm^G($4d`f<#3 zl}L_`g@*hJ2omGUDA^`C0Vg<0MpP#>5w1#ubyT7aC*)=c%tX?X6Pc*5N)u5!uZp?e zms}r$uMXJKdoVXJ#tD|r(M=wL8 zfU=pt(aRy?#_Upm-g1onMOMpo$ccoPYeu2ySo^JP#<7ZpBY}+7Bcg7*%COlFYMNav zv8>73A;KjL_4N1`Gv_Xp@Rp|E`|9TP%4bvj*wl9?ylV9L!&}jPJQx5 zX9+X7b5_gs4`*1xDh#-qS9>fCawyqAtrRWl>fn+&P-KTws;fGdlQUg^-n8w^fC+VF z9x`)m3oLRs_-Mw-Y%gJ|PhIcKKE1Ym6{!F=%R>d;q=LAzKG zy}@mPut8t3A7sUn13#F*-eGL^Eb4dML*ee&+w3{Ck(B&^DvH+k~j5+I@@b#mcjn0_d`)gh`l0!p3knPq z?e_-1t~4e{97@gmf*^#V%W@LD-19iPuo7Xm@|mb!F%`|>36Q>o8mswF!Bk}pl20OF zp|afw%K=g$5l^-U8Cb^*0wA%c5iks{RvLmEHW@Z5W+3cC5CQQq(MUv8@Jwtig79HZ zFRAl(@p{T0em2xf_VX`8A9P{n{r4fTL0hQN#`EEn%60$3^$!gjxKf$Zwhm_ip}f{SRafH=-SycQyP( zw1=$VSEF5>!jJ&b(qm`{o&bD`ydLu^|0DVksa)-eog$$@^zA6ZGyFz8t>uth!%1Q( z*~ynD`WOE@+u)t?S&o_ajx(EZ=lS)-s>&%?dmH~RV6P*HD?W)-2_O~m5^fY3m|W#brVq}p7+VbEgMFxZQ@Vwgwg?W=g*MHO8|ytix}3bP29S?mCUNYM&XxRqyC y*cKpc>uIk6^V)rGP*JDH<*aRM`g(tpu0;16<*N#C31IwGsyoz=aF%&01E7dcfBF^xkjhy?OKI zH}m+!-!o}1l`jW;XIRPUW6y;&q*83zRx1!KNe5h9%PIHwy)iGXBt7McftJCTc%zT)4)K_Pp!z8e> z@3Ek^dVAl>N8!sM7`_Z(;7VowmwF@>)$IqY-e%)`S}RW;hY2DO=1ib{3_+A{f|cei z?a_=V3HJ$Q>P?Tjjaq%*M#Ds9-EFzI+N^5xZT*AAvzX%~Ko}RZqsq=8iH}R_Y>3*exmOu=cp-& zBoL&V+>v^ONP!u|cF3*_R%!_zx`9%sz+Us86bg60B36=4x!7Qjx5 z(%D5MbOa6IB?PQCR5#mDt;_K6*lP%{Bj9#yg$Z?BMgk?mUPcf#91ut<$`T2s$~);X za+QCZ{-%6-5P_OJxCv~8$X?>_4cQ5}9-C`xuJ7|NhDH|#%6#gIAZ#~qqXn^SA2+AV zIcTFv*fsuW=%@eN#_L$wJpVOwV(CB|g)ZQ>t>6rFZ1rhP6iSE`a1Lkg>kxl4J3*Ft zD_bJh`KQ@GRnf;q{&DVSQsI~KlWNp+-p#V4m9BVHbVi$5sMTOVP%u~4hksJB+Du~#sH@_$Muo#A{$KZ~Z43gGi Pq+ysaG#p@LtTg!-tnPz* diff --git a/modules/routes/__pycache__/api_notifications.cpython-310.pyc b/modules/routes/__pycache__/api_notifications.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..575995dceeeddbe7af19918c35ddb736e0e4cb37 GIT binary patch literal 3792 zcmaJ^+ixRR8J{yZk1xqO&fa&sYiNUNXyTT;LWnlqBs5a%EyO~?Qe|d+#@R_`Jl-?o z?#7x(fuy@B(y~PY+QK4i1cC)=DZI4oKJ!l;@kX6&o_K%<)F;00jP2M-%0%b*%y%yH zo!|HSF7ZZ25)wQQUikLv-$y0spZGHPsPJ+Hn*R^zs8o_Dp|V3tMEuGn8GaQ8j1n!YE~F`5zbaUmb?%TLl%LVi-nPt!Bq z^=IihAwNYg(Tnt%ZBjZ-%xsib@CiXXfvG?oW?LBJmy>ll!_xe&|AEZ(tN4P=F zzzS^F$|*elveU5Y%&rAI#;ltS%ZI_L@73(;Ixd}`E6$bYm%fvedED_#T86zQkNf7E zRx}(l>vp;31$Nc0n1Sup{AOn8bqY^wX7aEuKQvK6eR_`q0}a|$X#V%13#5Q-kqs%3 zSCy8EMrzAcS(7(Npmk*`)!H(wkwd~RQyqa0rFj4wcIlx+4I1B;zyQg63P@rgNrm!T z@;O)mFpUV9p8oj5&i$W9Sao*ZecJv5*BdovnUs?kIeCd|j_umP{ue=|3W2Wo$S}~M zEkpbFMd()G<8LH3NJv`1FG||74~qRnz6myYD03ChTzp8lvSJ1BE>FSnwTBWr1A9N2 zzU8qsKVLVQ=}z8gT%WA32RFQ0-t}n1vHU#q8Ug$q!FsB`&J#0^?FSXltEQUvB3fGEDi%VDM zic8lD%jLzTR~Hzn%2kuC_-q`93bI)DJq8*wBopv^WQcM+8QHe%)tq%U0!t35&f*|T zL-QeqNQRP8GDMX}Nk&P_&C5qQoR7|%tay!DFhppy0+Lz~`g1TiAT%P{qbiV<1PWC` zVgucQmTChbjA*(0;EzxL{3Igh?$18hzk7ekT!R#3Ctw#t&4pRmNgN@y4$Q*N!7FOQ zkS5LN&>melg!;gWBlO^!n98Zb3JiC1#j$D;yfXVd9`^!v!{6fR!>4~17WwEf04hll zHU-0G?ifU81}#>$>1Q!)38VhA=%t(RS(%Mo>$M6l31V~~5xb>DyB-oRrv9767 zjZ^Tc-PYZwk3NWu^zlcJJ0JcPY&N7_j|yKx?XuAQ07FF*Lj{%2NjuV-%pg>JS0d6) zNJBsZ2oE+bM3|x_!OKn@g5|)jJU+8x)vVifHjAM82!yaNs9n6W^Pha3?2dcH))kiPxqlmeENeAwVdI z`Hf1&^8K!}Ml?}Dr9kk6Apl&ddAGRX2ds*zhZ!KtDGV_YEaIub3r4rYpvy!=85z0? zvd%}Y8_HRtk~A?$h9r}g3;+v3lQ#uy zvoOWbd+T8tB zJ2IXSM|1ao2$^eIjB#yX2aYwJjh2CD?EmJy{+tUT#9ZmmcJ92@dGxpbtOFH+Go9Ug z{QJ(%pR$Nh{+7vVc5Njuf>^#Ag!&tx9JN+FwmuCC-r4)hlWc^iU^$c*wskA-2T-#5 zfnD+YyRg0m{odd1{HO(*@7{a#Jhdx9PAn+wBTW~Dzk-({&A2A6gn3D54+SGC%8lOV zxr#>$PiI&U+5dr{I>he5vWsEFU`X*Ok6*^ouxGQ+fNT}zm{Xe*QBC%#-0fCY>{(cx zOAX|4wb8IC*RcOGS8iCh4}}@7oAtU?qihntmBUU1?HHTK@d9>>*a=^H1xKPBn8DGo z%6$A7B{AUom!LC}X+oZ&4eHx<#ZJhC!2E!45SXxN$$yKp`9z@Uv)Q|9R+CaCHp6C!wX`euiu) R31W;T$Y=~&3R)6c;(xGjxbXl0 literal 0 HcmV?d00001 diff --git a/modules/routes/__pycache__/api_reviews.cpython-310.pyc b/modules/routes/__pycache__/api_reviews.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44b9b1868418e7b77b86986652e5bff7355a5605 GIT binary patch literal 4633 zcmb_f>u(g-6`wng-PxDDUW~DMgiRG!Y!vLYr7z^Vp@Af{U}%6!*(%zu_m0iLzPK|B zcr{3*m`77lXiL*71$9?S+M-G!Nu|<))cha!Yq7Dv^h?waMXJ=_xwC8ULiiG8S99jh zx#zyl`JKnDrBZPXzfWHH;fc35XxhK%qxWax<0U-azd^W`*Er*PndO;!>vbc$`NUwF@l2QE?LBBza14MigfNoI##eoJ~&V#4sP? znR&)HT-Et7->CSbia)Z(-^521e~gdu&5K%oGk=C}<=Ylnehb%+Xu0i;3=SQCaPLn` zzkX}!gZ6Uk@7X==hmu_v-j>fx~H&r|LZbw@X+3U2aXh8*?(Z4jJc7yX7Oqn(pTtL#=> zf0Z^Tjbgc6VrX@LwEqYmz*Y0IAoE(Fy_r160@eaVIcw`zw3E8f1NKY(6byM=7XuIz zgCMt=H0B-uwl4LF7bO$7nd-Z(J=%4qCQf>j^`a-9WPGaZdVZ-^sZVdl^q+%Rw|ag>4|mOz5~(W$j^@8@sc2c zhJz?GKyk*5jF*H1r13)0mzo%c1i;763R*EFThNmBRJmB0<;Cf3`0{8cNv*lxIFEQWfn5m#6Xu~Q z2#_L;?{HgV)S|tK6bSJ>vV?667#U{k8J029dNb1t)yYz=QT3kyEP?f98vkXGb-?hs;RJ5uzp@rC(S0&Q@!Fb`Burt2f}eeHJsZiXWQH5MRMEq1__krotV)NNlyO z9)oq3e;$1vC1l$eWhP5An~87ZvzdLG(ZH{Z+G$6chYuY&im(u^I0#$Z>r-tMbF5nv zyZ$`Z40I%iDDte0B#=|KjwxHm$=0eDdHI~-TY&*zGy~I{Qc}rvDPIdr^jS(eDBY^u6%Mi%wv7Z?!~d#2}wbyTSuu-2v9LYg~qR<_gf@Ezz@Sk$Ecl*{#l?D zyJ*DMiF|{|T7vrz)hsJ_-hX)GuPb-%F5Ufj>Al5=*DtMJht*}Ggfuc=6ASS7X8Kt5 zWVLptn(Z>BNp*&p#00B!!bd~nZ|oee>>TIW@z-{bAKX2DL_7ojQ5ik$dhTr5k#=XN zT%sgVKvYS}Zg`HU7AualioD`hdwORVC{=ikQVAuIG+p1Ruou)sf({`l{^g?U-YAr=r)gM> z&Kpi_(MCKOqnBFmac9j?tmwB3nZP6Kb-i^!PqVmThn{OBS=tz3$aDB-SX_@Y3d4|K z1G+ehVa@GN^=jdqa7un3tSHN_`S8d1_~L!ow_1_5R-bUSb4Ea=V1g&%p(amtd?wnm z;e#=bY8#wE_(&X5X+ETUCIgQ%!G}-!i5}lU{oTpPBg%7@fBvh7_iueco>PP-LeJ?$ zb5h*raU4NePJ9PMB3>6!LyPV7VTZOrowJ8N6Wvb~V`3i-d8MB{yC74c40fTWc?|+b zbIGTIJgly4LOPZ13esYRranZ30IZ;#vmQtBcd6?b5fx6JNG(0OcoR|{RVKY)P3p-G zC~d$psf!;&pbvHAUW7VTdr|#BsPmD2ZyHFt_5^5+p?pQ0)C2AJ8aHkkTkwqv z>{$#6^Zvt+?k`>W%hH{HEZw{D@ZCk!Var!PSbpd7gZqEW8O>PH-HA%uDI+G#*Krpx zOGG2eEelOcU22ePZOkWPL?}%PN^Ch@C~r>8Hr%rBRtuG4J=Di~jQu|38hBLssT(@e zS>%6q#DXXJ9XSPHK?fyvXw2nIXVKh4KX8L-|KzdU5B$b|^gg_6BdIky03|*)d z8%)uut+u#H&0JG*q{5){0D?^-W~w%yrq%23xkxLfjbYXV-aAxh5f?DDPm zm+xKA;`|hi*1H?CxPtkPfP~uJ%K3|`b4I*`ZOd4(TB|NpY7I}_tCVvf%u@1D<`9HU zx+2gjxVv_z3o{j^kzBmTBYr^R4iGs=gkaa>0;K=Y1tRN{i&^59h){JL#`XSETn>fn z7UpWA6B8~$)Efn{<7tHhAOUDdvdDkq@i;Se!O;m!{RFR>+*TPa6nL#vC') def api_items(category_id): items_file = DATA_DIR / f'items_{category_id}.json' @@ -49,6 +57,23 @@ def api_item_detail(category_id, item_id): @items_bp.route('/api/items/', methods=['POST']) def api_create_item(category_id): data = request.get_json() + + # 审核模式 + if is_review_required(): + from modules.routes.api_reviews import submit_for_review + from modules.routes.api_notifications import create_notification + + review = submit_for_review(category_id, data, source='web') + product_name = data.get('name', '未知') + create_notification( + title='新产品待审核', + message=f'有新的产品"{product_name}"待审核', + level='warning', + category='review', + data={'review_id': review['id'], 'category': category_id} + ) + return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']}) + items_file = DATA_DIR / f'items_{category_id}.json' items = load_data(items_file) data['id'] = uuid.uuid4().hex[:12] diff --git a/modules/routes/api_models.py b/modules/routes/api_models.py index 6287dcd..0334bcf 100644 --- a/modules/routes/api_models.py +++ b/modules/routes/api_models.py @@ -11,6 +11,15 @@ from utils import load_data, save_data, parse_date_to_timestamp, safe_sort_key models_bp = Blueprint('api_models', __name__) +def is_review_required(): + """检查是否需要审核""" + try: + import app as app_module + return getattr(app_module, 'REQUIRE_REVIEW', False) + except: + return False + + @models_bp.route('/api/models') def api_models(): models = load_data(MODELS_FILE) @@ -47,6 +56,24 @@ def api_model_detail(model_id): @models_bp.route('/api/models', methods=['POST']) def api_create_model(): data = request.get_json() + + # 审核模式:提交到审核队列 + if is_review_required(): + from modules.routes.api_reviews import submit_for_review + from modules.routes.api_notifications import create_notification + + review = submit_for_review('ai-models', data, source='web') + product_name = data.get('name', '未知') + create_notification( + title='新产品待审核', + message=f'有新的AI模型"{product_name}"待审核', + level='warning', + category='review', + data={'review_id': review['id'], 'category': 'ai-models'} + ) + return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']}) + + # 直接创建模式 models = load_data(MODELS_FILE) data['id'] = uuid.uuid4().hex[:12] data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') diff --git a/modules/routes/api_notifications.py b/modules/routes/api_notifications.py new file mode 100644 index 0000000..aa4835a --- /dev/null +++ b/modules/routes/api_notifications.py @@ -0,0 +1,109 @@ +""" +通知管理 API +""" +import uuid +import json +from datetime import datetime +from flask import Blueprint, request, jsonify +from config import DATA_DIR +from utils import load_data, save_data + +notifications_bp = Blueprint('api_notifications', __name__) + +NOTIFICATIONS_FILE = DATA_DIR / 'notifications.json' + + +@notifications_bp.route('/api/notifications') +def api_notifications(): + """获取通知列表""" + notifications = load_data(NOTIFICATIONS_FILE) + + # 筛选参数 + unread_only = request.args.get('unread', '0') == '1' + limit = int(request.args.get('limit', 50)) + + if unread_only: + notifications = [n for n in notifications if not n.get('read', False)] + + # 按时间倒序 + notifications = sorted(notifications, key=lambda x: x.get('created_at', ''), reverse=True) + + return jsonify(notifications[:limit]) + + +@notifications_bp.route('/api/notifications/unread-count') +def api_unread_count(): + """获取未读通知数量""" + notifications = load_data(NOTIFICATIONS_FILE) + count = len([n for n in notifications if not n.get('read', False)]) + return jsonify({'count': count}) + + +@notifications_bp.route('/api/notifications//read', methods=['POST']) +def api_mark_read(notification_id): + """标记通知为已读""" + notifications = load_data(NOTIFICATIONS_FILE) + notification = next((n for n in notifications if n['id'] == notification_id), None) + if not notification: + return jsonify({'error': 'Notification not found'}), 404 + notification['read'] = True + notification['read_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + save_data(NOTIFICATIONS_FILE, notifications) + return jsonify({'success': True}) + + +@notifications_bp.route('/api/notifications/read-all', methods=['POST']) +def api_mark_all_read(): + """标记所有通知为已读""" + notifications = load_data(NOTIFICATIONS_FILE) + for n in notifications: + if not n.get('read', False): + n['read'] = True + n['read_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + save_data(NOTIFICATIONS_FILE, notifications) + return jsonify({'success': True}) + + +@notifications_bp.route('/api/notifications/', methods=['DELETE']) +def api_delete_notification(notification_id): + """删除通知""" + notifications = load_data(NOTIFICATIONS_FILE) + notifications = [n for n in notifications if n['id'] != notification_id] + save_data(NOTIFICATIONS_FILE, notifications) + return jsonify({'success': True}) + + +# ─── 内部函数:创建通知 ─────────────────────────────────────────────────────── + +def create_notification(title, message, level='info', category='system', data=None): + """ + 创建一条通知 + + 参数: + title: 通知标题 + message: 通知内容 + level: 级别 info/warning/error/success + category: 分类 system/api/review/statistics + data: 附加数据(dict) + """ + notifications = load_data(NOTIFICATIONS_FILE) + + notification = { + 'id': uuid.uuid4().hex[:12], + 'title': title, + 'message': message, + 'level': level, # info, warning, error, success + 'category': category, # system, api, review, statistics + 'data': data or {}, + 'read': False, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + notifications.append(notification) + + # 只保留最近500条通知 + if len(notifications) > 500: + notifications = sorted(notifications, key=lambda x: x.get('created_at', ''), reverse=True)[:500] + + save_data(NOTIFICATIONS_FILE, notifications) + return notification \ No newline at end of file diff --git a/modules/routes/api_reviews.py b/modules/routes/api_reviews.py new file mode 100644 index 0000000..56b4b4a --- /dev/null +++ b/modules/routes/api_reviews.py @@ -0,0 +1,159 @@ +""" +产品审核 API +""" +import uuid +import json +from datetime import datetime +from flask import Blueprint, request, jsonify, session +from config import DATA_DIR, MODELS_FILE, GPUS_FILE, CPUS_FILE +from utils import load_data, save_data + +reviews_bp = Blueprint('api_reviews', __name__) + +PENDING_FILE = DATA_DIR / 'pending_reviews.json' + + +@reviews_bp.route('/api/reviews') +def api_reviews(): + """获取待审核列表""" + reviews = load_data(PENDING_FILE) + + # 筛选参数 + status = request.args.get('status', 'pending') # pending, approved, rejected, all + limit = int(request.args.get('limit', 100)) + + if status != 'all': + reviews = [r for r in reviews if r.get('status', 'pending') == status] + + # 按时间倒序 + reviews = sorted(reviews, key=lambda x: x.get('created_at', ''), reverse=True) + + return jsonify(reviews[:limit]) + + +@reviews_bp.route('/api/reviews/count') +def api_reviews_count(): + """获取待审核数量""" + reviews = load_data(PENDING_FILE) + pending_count = len([r for r in reviews if r.get('status', 'pending') == 'pending']) + return jsonify({'count': pending_count}) + + +@reviews_bp.route('/api/reviews/') +def api_review_detail(review_id): + """获取审核详情""" + reviews = load_data(PENDING_FILE) + review = next((r for r in reviews if r['id'] == review_id), None) + if not review: + return jsonify({'error': 'Review not found'}), 404 + return jsonify(review) + + +@reviews_bp.route('/api/reviews//approve', methods=['POST']) +def api_approve_review(review_id): + """通过审核""" + reviews = load_data(PENDING_FILE) + review = next((r for r in reviews if r['id'] == review_id), None) + if not review: + return jsonify({'error': 'Review not found'}), 404 + + if review.get('status') != 'pending': + return jsonify({'error': '该申请已处理'}), 400 + + # 获取数据文件 + category_id = review.get('category_id') + data_file = get_data_file(category_id) + if not data_file: + return jsonify({'error': 'Unknown category'}), 400 + + # 添加数据到正式文件 + items = load_data(data_file) + item_data = review.get('data', {}) + + # 确保有ID和时间戳 + if 'id' not in item_data or not item_data['id']: + item_data['id'] = uuid.uuid4().hex[:12] + item_data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + item_data['visible'] = True + item_data['approved'] = True + item_data['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + items.append(item_data) + save_data(data_file, items) + + # 更新审核状态 + review['status'] = 'approved' + review['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + review['approved_by'] = session.get('username', 'admin') + save_data(PENDING_FILE, reviews) + + return jsonify({'success': True, 'item': item_data}) + + +@reviews_bp.route('/api/reviews//reject', methods=['POST']) +def api_reject_review(review_id): + """拒绝审核""" + reviews = load_data(PENDING_FILE) + review = next((r for r in reviews if r['id'] == review_id), None) + if not review: + return jsonify({'error': 'Review not found'}), 404 + + if review.get('status') != 'pending': + return jsonify({'error': '该申请已处理'}), 400 + + data = request.get_json() or {} + reason = data.get('reason', '') + + # 更新审核状态 + review['status'] = 'rejected' + review['rejected_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + review['rejected_by'] = session.get('username', 'admin') + review['reject_reason'] = reason + save_data(PENDING_FILE, reviews) + + return jsonify({'success': True}) + + +def get_data_file(category_id): + """获取类别对应的数据文件""" + builtin_map = { + 'ai-models': MODELS_FILE, + 'gpus': GPUS_FILE, + 'cpus': CPUS_FILE + } + + if category_id in builtin_map: + return builtin_map[category_id] + + # 动态分类 + return DATA_DIR / f'items_{category_id}.json' + + +# ─── 内部函数:提交审核 ─────────────────────────────────────────────────────── + +def submit_for_review(category_id, data, source='web', submitter=None): + """ + 提交产品到审核队列 + + 参数: + category_id: 分类ID + data: 产品数据(dict) + source: 来源 web/api + submitter: 提交者 + """ + reviews = load_data(PENDING_FILE) + + review = { + 'id': uuid.uuid4().hex[:12], + 'category_id': category_id, + 'data': data, + 'source': source, + 'submitter': submitter or 'anonymous', + 'status': 'pending', # pending, approved, rejected + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + reviews.append(review) + save_data(PENDING_FILE, reviews) + + return review \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html index 3f5c98b..875b603 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -41,6 +41,21 @@

管理概览

+ + + +
加载中...

快捷操作

@@ -275,6 +290,49 @@
+ + + + + + @@ -766,6 +824,24 @@ `; + // 通知中心(显示未读数) + html += ` + + + 通知中心 + + + `; + + // 审核管理(显示待审核数) + html += ` + + + 审核管理 + + + `; + document.getElementById('sidebarNav').innerHTML = html; } @@ -882,6 +958,8 @@ if (section === 'gpus') loadAdminGpus(); if (section === 'cpus') loadAdminCpus(); if (section === 'knowledge') loadAdminKnowledge(); + if (section === 'notifications') loadNotifications(); + if (section === 'reviews') loadReviews(); } // 加载网站配置 @@ -990,6 +1068,9 @@ document.getElementById('recent-models').innerHTML = models.length > 0 ? models.map(m => `
${m.name}${m.organization}
${m.is_open_source ? '开源' : '商业'}
`).join('') : '
暂无数据
'; + + // 加载通知计数 + loadNotificationCounts(); } // 内置分类列表 @@ -2993,6 +3074,232 @@ } catch (e) { alert('导入失败: ' + e.message); } } + // ─── 通知和审核功能 ───────────────────────────────────────────────── + + // 加载未读通知和待审核数 + async function loadNotificationCounts() { + try { + const [notifRes, reviewRes] = await Promise.all([ + fetch('/api/notifications/unread-count'), + fetch('/api/reviews/count') + ]); + const notifData = await notifRes.json(); + const reviewData = await reviewRes.json(); + + // 更新导航栏徽章 + const notifBadge = document.getElementById('navNotificationBadge'); + const reviewBadge = document.getElementById('navReviewBadge'); + + if (notifData.count > 0) { + notifBadge.textContent = notifData.count > 99 ? '99+' : notifData.count; + notifBadge.classList.remove('hidden'); + } else { + notifBadge.classList.add('hidden'); + } + + if (reviewData.count > 0) { + reviewBadge.textContent = reviewData.count > 99 ? '99+' : reviewData.count; + reviewBadge.classList.remove('hidden'); + } else { + reviewBadge.classList.add('hidden'); + } + + // 更新概览页的待办提醒 + const todoAlert = document.getElementById('todoAlert'); + const totalCount = notifData.count + reviewData.count; + if (totalCount > 0) { + todoAlert.classList.remove('hidden'); + document.getElementById('todoCount').textContent = totalCount; + document.getElementById('todoDetail').textContent = + `${notifData.count} 条未读通知,${reviewData.count} 条待审核产品`; + } else { + todoAlert.classList.add('hidden'); + } + } catch (e) { + console.error('加载通知数失败:', e); + } + } + + // 加载通知列表 + async function loadNotifications() { + try { + const res = await fetch('/api/notifications?limit=50'); + const notifications = await res.json(); + + if (notifications.length === 0) { + document.getElementById('notificationsList').innerHTML = ` +
+ +
暂无通知
+
+ `; + return; + } + + const levelColors = { + info: 'bg-blue-50 border-blue-200', + warning: 'bg-orange-50 border-orange-200', + error: 'bg-red-50 border-red-200', + success: 'bg-green-50 border-green-200' + }; + const levelIcons = { + info: 'ri-information-line text-blue-600', + warning: 'ri-alert-line text-orange-600', + error: 'ri-error-warning-line text-red-600', + success: 'ri-checkbox-circle-line text-green-600' + }; + + const html = notifications.map(n => ` +
+
+ +
+
${n.title}
+
${n.message}
+
${n.created_at}
+
+ ${!n.read ? `` : ''} +
+
+ `).join(''); + + document.getElementById('notificationsList').innerHTML = html; + } catch (e) { + document.getElementById('notificationsList').innerHTML = ` +
加载失败: ${e.message}
+ `; + } + } + + // 标记单个通知已读 + async function markRead(notifId) { + await fetch(`/api/notifications/${notifId}/read`, {method: 'POST'}); + loadNotifications(); + loadNotificationCounts(); + } + + // 标记全部已读 + async function markAllRead() { + await fetch('/api/notifications/read-all', {method: 'POST'}); + loadNotifications(); + loadNotificationCounts(); + } + + // 加载审核列表 + async function loadReviews() { + const status = document.getElementById('reviewStatusFilter').value; + try { + const res = await fetch(`/api/reviews?status=${status}`); + const reviews = await res.json(); + + if (reviews.length === 0) { + document.getElementById('reviewsTable').innerHTML = ` + 暂无数据 + `; + return; + } + + const statusLabels = { + pending: '待审核', + approved: '已通过', + rejected: '已拒绝' + }; + + const html = reviews.map(r => { + const catName = categories.find(c => c.id === r.category_id)?.name || r.category_id; + return ` + + ${r.data?.name || '-'} + ${catName} + + ${r.source === 'api' ? 'API' : '网页'} + + ${r.created_at} + ${statusLabels[r.status] || r.status} + + ${r.status === 'pending' ? ` + + + + ` : ` + + `} + + + `; + }).join(''); + + document.getElementById('reviewsTable').innerHTML = html; + } catch (e) { + document.getElementById('reviewsTable').innerHTML = ` + 加载失败: ${e.message} + `; + } + } + + // 通过审核 + async function approveReview(reviewId) { + if (!confirm('确认通过该产品审核?')) return; + try { + const res = await fetch(`/api/reviews/${reviewId}/approve`, {method: 'POST'}); + const data = await res.json(); + if (data.error) { + alert('操作失败: ' + data.error); + } else { + alert('审核通过,产品已发布!'); + loadReviews(); + loadNotificationCounts(); + loadOverview(); + } + } catch (e) { + alert('操作失败: ' + e.message); + } + } + + // 拒绝审核 + async function rejectReview(reviewId) { + const reason = prompt('请输入拒绝原因(可选):'); + if (reason === null) return; // 用户取消 + try { + const res = await fetch(`/api/reviews/${reviewId}/reject`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({reason}) + }); + const data = await res.json(); + if (data.error) { + alert('操作失败: ' + data.error); + } else { + alert('已拒绝该产品!'); + loadReviews(); + loadNotificationCounts(); + } + } catch (e) { + alert('操作失败: ' + e.message); + } + } + + // 查看审核详情 + async function viewReviewDetail(reviewId) { + try { + const res = await fetch(`/api/reviews/${reviewId}`); + const review = await res.json(); + + let html = '
'; + html += `
状态: ${review.status}
`; + html += `
分类: ${review.category_id}
`; + html += `
来源: ${review.source}
`; + html += `
提交时间: ${review.created_at}
`; + html += '

产品数据:

'; + html += '
' + JSON.stringify(review.data, null, 2) + '
'; + html += '
'; + + alert(html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()); + } catch (e) { + alert('加载失败: ' + e.message); + } + } + init();