From e514a11e62bc895a7e9d4d2f85424295e8329477 Mon Sep 17 00:00:00 2001 From: Jeason <1710884619@qq.com> Date: Tue, 17 Mar 2026 17:05:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=A0=81=20+=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E7=B3=BB=E7=BB=9F=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User 模型新增 is_admin 字段 新增 InviteCode 模型(邀请码表) 注册接口必须提供有效邀请码,使用后自动标记 管理员接口:查看所有用户、启用/禁用用户、生成/删除邀请码 前端新增管理面板页面 /admin,导航栏对管理员显示入口 注册页面新增邀请码输入框 选择性超话签到: 新增 GET /api/v1/accounts/{id}/topics 接口获取超话列表 POST /signin 接口支持 {"topic_indices": [0,1,3]} 选择性签到 新增超话选择页面 /accounts/{id}/topics,支持全选/手动勾选 账号详情页新增"选择超话签到"按钮 --- .../__pycache__/accounts.cpython-311.pyc | Bin 22597 -> 24560 bytes backend/api_service/app/routers/accounts.py | 39 ++++- .../app/__pycache__/main.cpython-311.pyc | Bin 14120 -> 22433 bytes backend/auth_service/app/main.py | 156 +++++++++++++++++- .../schemas/__pycache__/user.cpython-311.pyc | Bin 6291 -> 6452 bytes backend/auth_service/app/schemas/user.py | 2 + backend/shared/models/__init__.py | 3 +- .../__pycache__/__init__.cpython-311.pyc | Bin 636 -> 659 bytes .../__pycache__/signin_log.cpython-311.pyc | Bin 1864 -> 1855 bytes .../models/__pycache__/user.cpython-311.pyc | Bin 2241 -> 3285 bytes backend/shared/models/user.py | 13 ++ backend/weibo_hotsign.db | Bin 45056 -> 110592 bytes create_sqlite_db.py | 4 +- frontend/app.py | 147 ++++++++++++++++- .../2029240f6d1128be89ddc32729463129 | Bin 9 -> 9 bytes .../841ecc86f9b7cf9085a6ad3204aa3a43 | Bin 335 -> 830 bytes .../f58a94e5e56b626ceb878155bdc43426 | Bin 0 -> 33 bytes frontend/templates/account_detail.html | 3 +- frontend/templates/admin.html | 115 +++++++++++++ frontend/templates/base.html | 3 + frontend/templates/register.html | 4 + frontend/templates/topics.html | 139 ++++++++++++++++ init-db-sqlite.sql | 15 ++ init-db.sql | 14 ++ init_mysql.sh | 10 +- weibo_hotsign.db | Bin 90112 -> 110592 bytes 26 files changed, 649 insertions(+), 18 deletions(-) create mode 100644 frontend/flask_session/f58a94e5e56b626ceb878155bdc43426 create mode 100644 frontend/templates/admin.html create mode 100644 frontend/templates/topics.html diff --git a/backend/api_service/app/routers/__pycache__/accounts.cpython-311.pyc b/backend/api_service/app/routers/__pycache__/accounts.cpython-311.pyc index e339a61951311180c3eeccfc1d100bd621cb66ea..5debed1cf7100863a33607e11d92d765078ef61d 100644 GIT binary patch delta 6108 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 C=A1kL delta 4479 zcmbVOdu*Fm75}d7&yMZb&ck^*N#i8V!)=BA#)jqi2p^!w~^ zzb|W3oVh?X6=+$j3y6)4Wk|~?duYgiATfzC9@7LvbT9<^gf=Aj10jU6p)pjZ!8!L! zoTiyr#ZCR|x#yhkoO91P_c8x6`Pqvk_)H++Q(*k!h3{v~ZC8W!jt^W{yN3!yp@h1% zay<|aNZO+X^-w${=`yWCuZ&kp+N)LR)$wXQ91rU?@fy80UMu-Ntxm6x*Xs@O20apw zNSk}lVp^;PjzdP}@T&I4LhZ;iJ~I;a)e^!9kWzB;~I?}&F0#i1}V(TRcL zU|n-WQJ#gLJ>uOUFQXw^F;1SVoO6H-ehab~t)kU5OlxTEc-c`t2beYAu!p~O?xk>@f}hQQPTBNXC3s~h6^rtd?$v4X-RHe-cGW`{+~46u_C+y>KIjG{o)7e zMdA{>+-r-bd(`2shD7kN_+?p^bc=1?DCrbq-b*AR{_1UOkAcv@Ef?iofL28!nM&oR zvSz}}C1@sP@;=e$+dw*tNnfqQji(9^ikb401M88*Ec1={p_?;fN;A1Mr~)U7!!}x# z7-wc8cP7i|qIteUGzY%x!_99g*9hNKd?WA}A$JKaG*swDtCiAfDwjK*VLXVOtqAuZ z>_(Y0NvY*d<#at`S{}~Q5Wy*4gYrIrn1>G{y%eJJG@G`3sm$al#!YtC7ry9iR~-wi}1#=v{A-q=APMelA|G8iO3KsgAJC zz~Xk!klKQ97XmKD@))V4mgEMkMAKy8G1pSKd^;*O0vIj;jLyzxK+72BVq6A4Dy~&u z1An98;@}Saq?CbcF21}-_F9))^J8fiYR*rHy)}nnk&o4UhvcL&%rxJJP$~-FgHxHi zA*6;8N={>Cw$tUgm!A0n)Vz1`YoiP(Tow*U+xP0i=<$^w)LIHI=L2XOJmu70frTXKr-p+QVFgUt(eSEjUdbGHgl7il)*m_9Lsgq;At*v z|0rragNQ>Ar*iW{K{NR=bOs3^Z}42seOI$g~G=t&T?k5CYPP<%NUug zVJ5RFX1V0WfE^VJ+713Z`kzH_YQoIu%+5U~l|O?Zb6QFw{~_|o%i_k}EwwTuz68xB zjLdj8ljYYzI8f}{HSelVIe}grxFOKQKR%_3t4*~czOO~>>NzTYwXa@`cI_0M`?rff zbw&f8rCZs!L)_POxA^t`jt1w1d(jZ}otAca3-#rlqGG6zIK{wFovP-Y6G1WG)2@2* z9-~iuYk$9JKIj+U7!LFYmOns8m2qXJjQY=#887^NGyc5i9%byGGv(AziB>Ta$OmRC z|o30QYY+ozt}38AhXWUqm&M7EaLbpG#A?M{mK&`z%3np^YY1~fjI8Y(2NK4=b}pSomMWvNPyC;NkiRNA z4y_IR6v&urAMwwNu|us@DVy`A)IoSnTsbscsOLLyavN7kE$TgtfV+x+1>q3{Jk#v= z8g?Zdh{kj_mC0GEp)oco_c?zWbwUXEB;xx~(W$X)Ay)Cf?;*==-#B~@KY`~TzCI0( z%K%RrJl^QEoul8P;XfdsaIg)PsDKI9_2#kgw$hW?sic-L8tt>_&!G7bl*PCTYx@iQ zombuqi@(SEiibuolA`v>-;v0*#z9qiMcsb4>in^b0K&^~Cwvzh6l2HMki58fEE=?Y z#%Hvo2K}N>bDwzWSX04|AqAJ%y*aM)a%2XKQ%TNfuMP*dW^mXw@aRU6iNfehDk(la zY3Af(nadl$dYLqrO=re=713-0aK`eV*Cb List[dict]: @@ -384,10 +403,12 @@ async def manual_signin( account_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), + body: dict = Body(default=None), ): """ - Manually trigger sign-in for all followed super topics. - Verifies cookie first, fetches topic list, signs each one, writes logs. + Manually trigger sign-in for selected (or all) super topics. + Body (optional): {"topic_indices": [0, 1, 3]} — indices of topics to sign. + If omitted or empty, signs all topics. """ account = await _get_owned_account(account_id, user, db) key = _encryption_key() @@ -421,6 +442,18 @@ async def manual_signin( "No super topics found for this account", ) + # Filter topics if specific indices provided + selected_indices = None + if body and isinstance(body, dict): + selected_indices = body.get("topic_indices") + if selected_indices and isinstance(selected_indices, list): + topics = [topics[i] for i in selected_indices if 0 <= i < len(topics)] + if not topics: + return success_response( + {"signed": 0, "already_signed": 0, "failed": 0, "topics": []}, + "No valid topics selected", + ) + # Sign each topic results = [] signed = already = failed = 0 diff --git a/backend/auth_service/app/__pycache__/main.cpython-311.pyc b/backend/auth_service/app/__pycache__/main.cpython-311.pyc index 8a87473af8c704207f8711a3b97f55c8f76fb6c1..a82adb356a14c56ff1c9ba5dd508448afa32f742 100644 GIT binary patch literal 22433 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 delta 3159 zcma)8Z){sv6@Tyf`A?jMH;IDf-qRbK-L5je3=*@kob@f2#E(mLrgHCNdyff1Ojo+ zv%Snon0Dj)yXT&B-nr+Vd+xdU#vA-=+Q4@_9ybT=?q7afI6uU3f5b_xIjh76Pm3D^ z^Xoh(^RiR%N?vR9Nj__Aky>DMDXpqs@~dr9o2A#F1k|7uv}m`|u69Tr7Tu_Hsv#+) zhNZ9?ks@lB)TKtHs2Y=EYPZy__DDUJUXv166H>yWJxZ_ICtd1O`=x$$KpIe!Qc@k1 z2Gt>Hi01^3v@9L1`Amklc#eA=dZkInKrYBfXI zamjwUEicFcIhg0MZ@06r^YJrjFySESdc(l85q~{zL6AF0qufb6a)?A09CG-!T^fZ2 z9>4_#>V%dPuK$P~H;y13(~3rH zp;*%7?+P>x@uj0|+!J?T;?pq8c?S6o_Oj>EU@I_8*8(l7nIct_PHJfkv-dsg_Ol=x zD_=cwhadkmem9M920!rVMwS|-s~K{QXa=1D-gD4(55Rlycaz(7bM}UtXX!K>^PYG# z27J>=OPW^D^0XUfNt@{eKd%*XggRhO6A0Ea`_>X^v1rg)5EJ%8Z<3#8zxAG*I!v=^ zkz^HPF_T*)xhr%IzEGj-K7fi-?=< zbVuw4E?8{_#<$v5?MrU983~9$*jTnF5^)&8nvmlTQB4m!jgCEyRmZARcFHazw8vXI zQlnSx?7xxNdYm&{H5SJuI4bbmEzanwy_Fga^r5Fa&rW@203NnqL z1mj6D=JYh1YfpI_=#w}ZMtB5(ZMKK!r*L)|;aLEaCpyLg{j*v%#VN@bbc4`L3H+p2 zK;v`J^>4#lQOTx%xH*6HYjk7sE0Z_R?>g9<9WU{IHqqJ7zSsHZ!M9FK`Odnyw_X0} zCfnQLp=qb>=T1A&HyTsflChXliur<;LfxZ+v$p~pF(m10i#7_1cG2ibAQZ0kaSQ5VYBs2q% zY^IpC)*`U4ASW`>kGMHzi%A-z)^`d zdo9(^pJF?y`S>9LsGM)VXmo zWuc&FN>rg=LWUz(RQ+nffSOGfDA5;9u~dRPCbK_{bnVZnk58wTYTUsreHmd5;f2a7 z*0xL$R=8?9msc_}x^W8?UPSnUr3KQGVpP)b72x>jJrr6AT)D+;BOPk99wrwUvGS3A z3v~X@mETHlh(dCbHO{h!O=k(-nHzYtYU*1M%xeq2v!wpXN3z#;g$XBh2F7F User: + """Dependency: require admin user.""" + payload = decode_access_token(credentials.credentials) + if not payload: + raise HTTPException(status_code=401, detail="Invalid token") + user_id = payload.get("sub") + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if not user or not user.is_active: + raise HTTPException(status_code=401, detail="User not found") + if not user.is_admin: + raise HTTPException(status_code=403, detail="需要管理员权限") + return user + + +@app.get("/admin/users") +async def admin_list_users( + admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """管理员查看所有用户""" + result = await db.execute(select(User).order_by(User.created_at.desc())) + users = result.scalars().all() + return { + "success": True, + "data": [ + { + "id": str(u.id), + "username": u.username, + "email": u.email, + "is_active": u.is_active, + "is_admin": u.is_admin, + "created_at": str(u.created_at) if u.created_at else None, + } + for u in users + ], + } + + +@app.put("/admin/users/{user_id}/toggle") +async def admin_toggle_user( + user_id: str, + admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """管理员启用/禁用用户""" + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + if str(user.id) == str(admin.id): + raise HTTPException(status_code=400, detail="不能禁用自己") + user.is_active = not user.is_active + await db.commit() + return {"success": True, "is_active": user.is_active} + + +@app.post("/admin/invite-codes") +async def admin_create_invite_code( + admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """管理员生成邀请码""" + code = secrets.token_urlsafe(8)[:12].upper() + invite = InviteCode(code=code, created_by=str(admin.id)) + db.add(invite) + await db.commit() + await db.refresh(invite) + return { + "success": True, + "data": { + "id": str(invite.id), + "code": invite.code, + "created_at": str(invite.created_at), + }, + } + + +@app.get("/admin/invite-codes") +async def admin_list_invite_codes( + admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """管理员查看所有邀请码""" + result = await db.execute(select(InviteCode).order_by(InviteCode.created_at.desc())) + codes = result.scalars().all() + return { + "success": True, + "data": [ + { + "id": str(c.id), + "code": c.code, + "is_used": c.is_used, + "used_by": c.used_by, + "created_at": str(c.created_at) if c.created_at else None, + "used_at": str(c.used_at) if c.used_at else None, + } + for c in codes + ], + } + + +@app.delete("/admin/invite-codes/{code_id}") +async def admin_delete_invite_code( + code_id: str, + admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """管理员删除未使用的邀请码""" + result = await db.execute(select(InviteCode).where(InviteCode.id == code_id)) + invite = result.scalar_one_or_none() + if not invite: + raise HTTPException(status_code=404, detail="邀请码不存在") + if invite.is_used: + raise HTTPException(status_code=400, detail="已使用的邀请码不能删除") + await db.delete(invite) + await db.commit() + return {"success": True} diff --git a/backend/auth_service/app/schemas/__pycache__/user.cpython-311.pyc b/backend/auth_service/app/schemas/__pycache__/user.cpython-311.pyc index 966539c4daf564e0ae826b7550ad1e5af7484b7c..4e7dcab46c01520a9357be0f4d2a07fa8762602f 100644 GIT binary patch delta 966 zcmZvaPe>F|9LMMF&g{<4?9RB#?6{MwiK$GPvdU(Ewm)LphPGx_hlP-EMp!I^-F2xh zSwwUQ+V~L!UL?>21B(?T=o}q9>_UNThYn)pB{cTXvG;px7}#Uphu`mg-*4XgePy928u1PC~o%QV7$vHux+xy1Hm&`l)Fe_~j;}m8PjS#9>4`q65)|ZmK4R zi2Zb?S7AcFq-w@=?6Fng4f+&$1K!ISV&=o%k0g2ox9f;nL^t9%;siy>nVig|(2R=T z`!I_9MF&KbITC>{%A&?z!-|ihTzLb`sE1@0q8lj_?vS6^xy}BCI9b+vncG|w+hpKgbNX6NAzNd=!5m@JERv*yMy^` z&23_wM`gEq08>^t$O>baBL<;adq!fgq75Eke-`_Jn6<2Z)&#pFJ1YjQfe^U}eSx`7 sCmAAJ-2a)Y-DXPpUtNMa{qjK{Y1rcSB6n;XSIfInJc%!fX0hjgB{Qv*} delta 832 zcmZvaKWGzC9LMja_b->r<&v0dF16N>NZV9JV@s?(YD#If{-Z_Fl0i@}g(5hlT?7Y* z4sIs=MsX3ULk5uuLQqFhT*N`4i$lT1C6NwJ#rJ(tBB_tN&;8!-``-6{zq|J`Psgoi z;jkg`@wxeGwQanxR%CXHmSO``l@F-Cc3B$LX-mG{cZommTPb>=MCfZYLUqOB3!6$o z{Ul%GSHo3Dn33jA6<{Bo*H&1XcH?g2qZVf@OW$iq7EQpA^e`C+}wQc~c2`9Lf|9@o6Ooi~)xMEUBQPN(vYNhW}!S2WzisQFAC} zdUV}fW5ejD@xy%1yc1Z~4xov)*f*hYB2_FV-f@)4fPNA#B9v3CJA> z^6aq@5l$j0D(b0n3IZ@e5A9oQ3WbpweWbzb&%b2j^dUNEOk*i9O|9rW{j^iG82jbT zAl+@i#&i}?0rBFVcNQT4v$PSv%ZiQNc#Re2k?s1N#Z*`7c?-DgON6XHI ztT|xLMf&b+<^yb*wWR;EnrsVY=U-i?H~m*~CL3x=dr_Ti QBfHa;_R~z#p{3;ZZ;-g5!~g&Q diff --git a/backend/auth_service/app/schemas/user.py b/backend/auth_service/app/schemas/user.py index b2c2e1f..2200925 100644 --- a/backend/auth_service/app/schemas/user.py +++ b/backend/auth_service/app/schemas/user.py @@ -16,6 +16,7 @@ class UserBase(BaseModel): class UserCreate(UserBase): """Schema for user registration request""" password: str = Field(..., min_length=8, description="Password (min 8 characters)") + invite_code: str = Field(..., min_length=1, description="注册邀请码") class UserLogin(BaseModel): """Schema for user login request""" @@ -35,6 +36,7 @@ class UserResponse(BaseModel): email: Optional[EmailStr] = None created_at: datetime is_active: bool + is_admin: bool = False wx_openid: Optional[str] = None wx_nickname: Optional[str] = None wx_avatar: Optional[str] = None diff --git a/backend/shared/models/__init__.py b/backend/shared/models/__init__.py index d8c6290..cb955d1 100644 --- a/backend/shared/models/__init__.py +++ b/backend/shared/models/__init__.py @@ -1,7 +1,7 @@ """Shared ORM models for Weibo-HotSign.""" from .base import Base, get_db, engine, AsyncSessionLocal -from .user import User +from .user import User, InviteCode from .account import Account from .task import Task from .signin_log import SigninLog @@ -12,6 +12,7 @@ __all__ = [ "engine", "AsyncSessionLocal", "User", + "InviteCode", "Account", "Task", "SigninLog", diff --git a/backend/shared/models/__pycache__/__init__.cpython-311.pyc b/backend/shared/models/__pycache__/__init__.cpython-311.pyc index 8f360034fbdb0de2a8120ea156dbfb97f1637752..5946dac5b6cc237e4663a1834c9924be52afa2bb 100644 GIT binary patch delta 284 zcmeyvGMSZkIWI340}#v<-I>`lkyny2Zlb!jQ7%Um2O~oYV+u0{;so(nN)n5+i$D&##Z{b{o|l;y zpOc>s)^>|MK0YxgCq8~9!)Ks8!!LaotC(b<6*1+hnMwIENr}nXsd*`rcQUGSi2>yq mfw*|m%$-8q9v0{FB`n_0(^%CnhK7 zm*$leaR9Bk#Zr=3oL$5TVsaH{rsrkm#pmRw7jaEqz$hYb;bIk&oS%{!Q=Xcclpm9n zn4F!Oml9KwT3k{bqo+6d6r(DaC{QaS5Eu7M{>f+}Aa;QP1xN diff --git a/backend/shared/models/__pycache__/signin_log.cpython-311.pyc b/backend/shared/models/__pycache__/signin_log.cpython-311.pyc index 37e5483f743ba5d97a171e8c2cd37d1e512b005a..c9ac3d4445b50744f747046b571a723f54751d1f 100644 GIT binary patch delta 37 scmX@Xx1W!DIWI340}$}P*|CxP9229_ETn8i3sQj1H9WAyYkFJ_*@3;-~X B4cPzy diff --git a/backend/shared/models/__pycache__/user.cpython-311.pyc b/backend/shared/models/__pycache__/user.cpython-311.pyc index 257278c61c7cf51c974a63698adc4b80a58f3598..08fc2344098bc7a5aeb9a1438a2b992d0d4ac546 100644 GIT binary patch delta 1162 zcmZvaO-vI(6vubk?Fapsc4?uM4{5+aODK(iMg!tEMI(4n(bR-mI*VE4Bbic+Tv!ug zdf?Dzh{41IC!~>s#*;U^da;c)Aw6*v6B7uWj0fj!1+_4f`Q^=f|F<*q-hPz6dhu(! z-6HVutm=beHD+)PdY)-jnsh=~AeIGzSY_L)UJ%~!RT>dYbhm|oE%V|D+Io-C1LPy7 zb-b$MBEAkpIzbj?2d8sfV$(VgThY1CiL#yOk7!+7 z%%Y(zyQwE=kG<1-4b}XpEc>i)Kw0)f-x$gw>f^5MivYr*>?5u~w4U2pRKy9H&dM}O z5^_M+wb)T}ZiUzsLy(WYp2G|o2(irF{a!+GlDSEmx7tSU9!H|P4B-qAY|n;a#v zzGe`60a`I?xwY-%=%**ZsfAm>YULOTNVF3+U4U-DX@CeY3iF)Mw4w-?Dq7-Hj?Ks< zI{LI|N+vXeYMnIkPJ)9PAkhry1)PWEk~B6`{)9`Q0QmSyTs{dsGD!P>80-IP4(>SZIJ^rhvMW$kWG>dH%91*uED zv5DQOvGj0yc=>WBvJ_c6nZuzx4i#`n4VPl~R^%LR&*Sz2Zdb$nA8P2(a_+B*IULO6 zU;zi!Fw9ap;kp4w(G-o6>3B3sU2M@b;@4jCe}jISdncz!azgH-w|EYMCl;99oKPfm h1?>pOGu6GTMfU%V`zuh#j&Kxd_bzz%j|pj){SAvc3bX(K delta 583 zcmcaAc~FpVIWI340}w3RwLSA6^F%%g#;l3znQSRc!3>&Q6E~GJ9^0(N7|mGE#E{CD z#Sc;m0?UASHJAk=*6=N30`h?v0-^+fs##J6Q<&HAqsa&XWrR}%QUp_k)(9_S2I>N0 zs4kHdkrsw1(G*dj)nd0;LW@(2Zn2gE$>NtPljWEbSTq^^G$%JPr_>jV0JStQ>`gwZ zazyK(&Jmr9cK%oF{2LgGKoXivx3~+6GIJA)D&w46iIUpwrWZdG2k1t9sD2k7-;sv=~55|?A?9Zyf4sw~Frr6{5C#z-_1t%AC$0?(l@{7YJH$SB`C)KXV2B;k9BZlHNlYep-I|?v5 UGk##ePIj1l1j~HEArE#X0LmzgZvX%Q diff --git a/backend/shared/models/user.py b/backend/shared/models/user.py index c1f78e0..e72b2ff 100644 --- a/backend/shared/models/user.py +++ b/backend/shared/models/user.py @@ -19,6 +19,7 @@ class User(Base): wx_openid = Column(String(64), unique=True, nullable=True, index=True) wx_nickname = Column(String(100), nullable=True) wx_avatar = Column(String(500), nullable=True) + is_admin = Column(Boolean, default=False) created_at = Column(DateTime, server_default=func.now()) is_active = Column(Boolean, default=True) @@ -26,3 +27,15 @@ class User(Base): def __repr__(self): return f"" + + +class InviteCode(Base): + __tablename__ = "invite_codes" + + id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) + code = Column(String(32), unique=True, nullable=False, index=True) + created_by = Column(String(36), nullable=False) + used_by = Column(String(36), nullable=True) + is_used = Column(Boolean, default=False) + created_at = Column(DateTime, server_default=func.now()) + used_at = Column(DateTime, nullable=True) diff --git a/backend/weibo_hotsign.db b/backend/weibo_hotsign.db index 5b8b19a457dc4c97566bf0c6d1b3363f292fef6a..032f316b730836722df356b891c24685b922d936 100644 GIT binary patch literal 110592 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/delete', methods=['POST']) +@admin_required +def delete_invite_code(code_id): + """删除邀请码""" + try: + resp = api_request('DELETE', f'{AUTH_BASE_URL}/admin/invite-codes/{code_id}') + data = resp.json() + if resp.status_code == 200 and data.get('success'): + flash('邀请码已删除', 'success') + else: + flash(data.get('detail', '删除失败'), 'danger') + except Exception as e: + flash(f'连接错误: {str(e)}', 'danger') + return redirect(url_for('admin_panel')) + + +@app.route('/admin/users//toggle', methods=['POST']) +@admin_required +def toggle_user(user_id): + """启用/禁用用户""" + try: + resp = api_request('PUT', f'{AUTH_BASE_URL}/admin/users/{user_id}/toggle') + data = resp.json() + if resp.status_code == 200 and data.get('success'): + status_text = '已启用' if data.get('is_active') else '已禁用' + flash(f'用户{status_text}', 'success') + else: + flash(data.get('detail', '操作失败'), 'danger') + except Exception as e: + flash(f'连接错误: {str(e)}', 'danger') + return redirect(url_for('admin_panel')) + + +# ===================== Topic Selection Signin ===================== + +@app.route('/accounts//topics') +@login_required +def account_topics(account_id): + """获取超话列表页面,供用户勾选签到""" + try: + resp = api_request('GET', f'{API_BASE_URL}/api/v1/accounts/{account_id}/topics') + data = resp.json() + topics = data.get('data', {}).get('topics', []) if data.get('success') else [] + + # 获取账号信息 + acc_resp = api_request('GET', f'{API_BASE_URL}/api/v1/accounts/{account_id}') + acc_data = acc_resp.json() + account = acc_data.get('data') if acc_data.get('success') else None + + if not account: + flash('账号不存在', 'danger') + return redirect(url_for('dashboard')) + + return render_template('topics.html', account=account, topics=topics, user=session.get('user')) + except Exception as e: + flash(f'获取超话列表失败: {str(e)}', 'danger') + return redirect(url_for('account_detail', account_id=account_id)) + + +@app.route('/accounts//signin-selected', methods=['POST']) +@login_required +def signin_selected(account_id): + """签到选中的超话""" + try: + indices = request.json.get('topic_indices', []) + resp = api_request( + 'POST', + f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin', + json={'topic_indices': indices}, + ) + data = resp.json() + if data.get('success'): + result = data.get('data', {}) + return jsonify({ + 'success': True, + 'data': result, + 'message': f"签到完成: {result.get('signed', 0)} 成功, {result.get('already_signed', 0)} 已签, {result.get('failed', 0)} 失败", + }) + else: + return jsonify({'success': False, 'message': data.get('message', '签到失败')}), 400 + except Exception as e: + return jsonify({'success': False, 'message': str(e)}), 500 + if __name__ == '__main__': debug_mode = os.getenv('FLASK_DEBUG', 'True').lower() in ('true', '1', 'yes') # use_reloader=False 避免 Windows 终端 QuickEdit 模式导致进程挂起 diff --git a/frontend/flask_session/2029240f6d1128be89ddc32729463129 b/frontend/flask_session/2029240f6d1128be89ddc32729463129 index ffb2cd9bfd75610267f6ba499787709ee5ac010d..04de15d01fe35710f04efd9b02c28a05a830f333 100644 GIT binary patch literal 9 QcmZQzU|?uq^=8on00XoE0{{R3 literal 9 QcmZQzU|?uq^=8%s00XiC0ssI2 diff --git a/frontend/flask_session/841ecc86f9b7cf9085a6ad3204aa3a43 b/frontend/flask_session/841ecc86f9b7cf9085a6ad3204aa3a43 index 585a8e0049f9f0c91a20f0ce7490c320a7037798..51c4341c514cc1c26d810d506a9eaba582c66667 100644 GIT binary patch delta 519 zcmXw!&2HL26ou8asX~7iRbQctZcJ^D2{@}HI7tWLaSbMbH`XvS4ui3ww!y|yMVDF3 za=PhT^aa{S=ri;wx~)O#>gv-y_uO-SpZt8@`2P0~|9blE!_yxP>&ad`+hi=2U*vmn z!l@MRXvb`ah5;1X(t% z!we>a7U{xs7_BGw-F?q&)~bq*;XxHSRs{TotJ*s)LLRL8o?AKNY3_J5^0_v5EW5H7 zl>44r`tg+ayFHEQ_q$nqoBPAsHNK5z8a;HO6h{2}?N7ln+vhM7P1w%{jng|11iq|O%Q<6Gw?P+)o tr!5!x(=G`=y6wTxW`m+qG|x$#ZLFiw;AVwy?jF=@8yWO7?XBge`43+4xrYD% delta 21 dcmdnTcAkmz=E;Yd4Xjfg7&mgdFfx_u0RUkO2Z{gy diff --git a/frontend/flask_session/f58a94e5e56b626ceb878155bdc43426 b/frontend/flask_session/f58a94e5e56b626ceb878155bdc43426 new file mode 100644 index 0000000000000000000000000000000000000000..50d665dcc638aef1ebd758c4a61214d3ab50352d GIT binary patch literal 33 kcmeDG{xGwFb*c~p1k_IH;fgOvEy_*IOU)~p(ow7j0KvWsiU0rr literal 0 HcmV?d00001 diff --git a/frontend/templates/account_detail.html b/frontend/templates/account_detail.html index d69bd84..8cbfdc0 100644 --- a/frontend/templates/account_detail.html +++ b/frontend/templates/account_detail.html @@ -86,8 +86,9 @@
- +
+ 🎯 选择超话签到 ⏰ 添加定时任务 diff --git a/frontend/templates/admin.html b/frontend/templates/admin.html new file mode 100644 index 0000000..3960f8b --- /dev/null +++ b/frontend/templates/admin.html @@ -0,0 +1,115 @@ +{% extends "base.html" %} + +{% block title %}管理面板 - 微博超话签到{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+

🛡️ 管理面板

+ +
+
+
{{ users|length }}
+
总用户数
+
+
+
{{ users|selectattr('is_active')|list|length }}
+
活跃用户
+
+
+
{{ invite_codes|rejectattr('is_used')|list|length }}
+
可用邀请码
+
+
+ +
+
+
+ 🎟️ 邀请码管理 +
+ +
+
+ {% if invite_codes %} + {% for code in invite_codes %} +
+
+ {{ code.code }} + {% if code.is_used %} + 已使用 + {% else %} + 可用 + {% endif %} +
+
+ {% if not code.is_used %} +
+ +
+ {% endif %} +
+
+ {% endfor %} + {% else %} +

暂无邀请码,点击上方按钮生成

+ {% endif %} +
+ +
+
👥 用户管理
+ {% for u in users %} +
+
+ {{ u.username }} + {% if u.is_admin %}管理员{% endif %} +
{{ u.email or '-' }}
+
+
+ {% if u.is_active %} + 正常 + {% else %} + 已禁用 + {% endif %} + {% if not u.is_admin %} +
+ +
+ {% endif %} +
+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/frontend/templates/base.html b/frontend/templates/base.html index b072d05..bea130d 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -223,6 +223,9 @@ 🔥 微博超话签到