full_server.py 147 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040
  1. #!/usr/bin/env python3
  2. """
  3. 完整的SSO服务器 - 包含认证API
  4. """
  5. import sys
  6. import os
  7. import socket
  8. import json
  9. import uuid
  10. # 添加src目录到Python路径
  11. sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
  12. # 加载环境变量
  13. from dotenv import load_dotenv
  14. load_dotenv()
  15. from fastapi import FastAPI, HTTPException, Depends, Request, Response
  16. from fastapi.middleware.cors import CORSMiddleware
  17. from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
  18. from pydantic import BaseModel
  19. from typing import Optional, Any, Union
  20. import hashlib
  21. import secrets
  22. # 修复JWT导入 - 确保使用正确的JWT库
  23. try:
  24. # 首先尝试使用PyJWT
  25. import jwt as pyjwt
  26. # 测试是否有encode方法
  27. test_token = pyjwt.encode({"test": "data"}, "secret", algorithm="HS256")
  28. jwt = pyjwt
  29. print("✅ 使用PyJWT库")
  30. except (ImportError, AttributeError, TypeError) as e:
  31. print(f"PyJWT导入失败: {e}")
  32. try:
  33. # 尝试使用python-jose
  34. from jose import jwt
  35. print("✅ 使用python-jose库")
  36. except ImportError as e:
  37. print(f"python-jose导入失败: {e}")
  38. # 最后尝试安装PyJWT
  39. print("尝试安装PyJWT...")
  40. import subprocess
  41. import sys
  42. try:
  43. subprocess.check_call([sys.executable, "-m", "pip", "install", "PyJWT"])
  44. import jwt
  45. print("✅ PyJWT安装成功")
  46. except Exception as install_error:
  47. print(f"❌ PyJWT安装失败: {install_error}")
  48. raise ImportError("无法导入JWT库,请手动安装: pip install PyJWT")
  49. from datetime import datetime, timedelta, timezone, date
  50. import pymysql
  51. from urllib.parse import urlparse
  52. # 导入RBAC API - 移除循环导入
  53. # from rbac_api import get_user_menus, get_all_menus, get_all_roles, get_user_permissions
  54. # 数据模型
  55. class LoginRequest(BaseModel):
  56. username: str
  57. password: str
  58. remember_me: bool = False
  59. class TokenResponse(BaseModel):
  60. access_token: str
  61. refresh_token: Optional[str] = None
  62. token_type: str = "Bearer"
  63. expires_in: int
  64. scope: Optional[str] = None
  65. class UserInfo(BaseModel):
  66. id: str
  67. username: str
  68. email: str
  69. phone: Optional[str] = None
  70. avatar_url: Optional[str] = None
  71. is_active: bool
  72. is_superuser: bool = False
  73. roles: list = []
  74. permissions: list = []
  75. class ApiResponse(BaseModel):
  76. code: int
  77. message: str
  78. data: Optional[Any] = None
  79. timestamp: str
  80. # 文档管理数据模型
  81. # --- 文档中心配置 ---
  82. TABLE_MAP = {
  83. "basis": "t_basis_of_preparation", # 编制依据
  84. "work": "t_work_of_preparation", # 施工方案
  85. "job": "t_job_of_preparation" # 办公制度
  86. }
  87. def get_table_name(table_type: Optional[str]) -> str:
  88. """根据类型获取对应的数据库表名,默认为编制依据"""
  89. return TABLE_MAP.get(table_type, "t_basis_of_preparation")
  90. class DocumentAdd(BaseModel):
  91. title: str
  92. content: str
  93. primary_category_id: Optional[Any] = None
  94. secondary_category_id: Optional[Any] = None
  95. year: Optional[int] = None
  96. table_type: Optional[str] = "basis" # 增加表类型参数
  97. class DocumentListRequest(BaseModel):
  98. primaryCategoryId: Optional[int] = None
  99. secondaryCategoryId: Optional[int] = None
  100. page: int = 1
  101. size: int = 50
  102. sort_by: str = "created_at" # created_at or updated_at
  103. # 配置
  104. JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key-12345")
  105. ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
  106. def check_port(port):
  107. """检查端口是否可用"""
  108. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  109. try:
  110. s.bind(('localhost', port))
  111. return True
  112. except OSError:
  113. return False
  114. def find_available_port(start_port=8000, max_port=8010):
  115. """查找可用端口"""
  116. for port in range(start_port, max_port + 1):
  117. if check_port(port):
  118. return port
  119. return None
  120. def get_db_connection():
  121. """获取数据库连接"""
  122. try:
  123. database_url = os.getenv('DATABASE_URL', '')
  124. if not database_url:
  125. return None
  126. parsed = urlparse(database_url)
  127. config = {
  128. 'host': parsed.hostname or 'localhost',
  129. 'port': parsed.port or 3306,
  130. 'user': parsed.username or 'root',
  131. 'password': parsed.password or '',
  132. 'database': parsed.path[1:] if parsed.path else 'sso_db',
  133. 'charset': 'utf8mb4'
  134. }
  135. return pymysql.connect(**config)
  136. except Exception as e:
  137. print(f"数据库连接失败: {e}")
  138. return None
  139. def verify_password_simple(password: str, stored_hash: str) -> bool:
  140. """验证密码(简化版)"""
  141. if stored_hash.startswith("sha256$"):
  142. parts = stored_hash.split("$")
  143. if len(parts) == 3:
  144. salt = parts[1]
  145. expected_hash = parts[2]
  146. actual_hash = hashlib.sha256((password + salt).encode()).hexdigest()
  147. return actual_hash == expected_hash
  148. return False
  149. def create_access_token(data: dict) -> str:
  150. """创建访问令牌"""
  151. to_encode = data.copy()
  152. expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
  153. to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
  154. encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm="HS256")
  155. return encoded_jwt
  156. def verify_token(token: str) -> Optional[dict]:
  157. """验证令牌"""
  158. try:
  159. payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
  160. return payload
  161. except jwt.PyJWTError:
  162. return None
  163. # 创建FastAPI应用
  164. app = FastAPI(
  165. title="SSO认证中心",
  166. version="1.0.0",
  167. description="OAuth2单点登录认证中心",
  168. docs_url="/docs",
  169. redoc_url="/redoc"
  170. )
  171. # 配置CORS
  172. app.add_middleware(
  173. CORSMiddleware,
  174. allow_origins=["*"],
  175. allow_credentials=True,
  176. allow_methods=["*"],
  177. allow_headers=["*"],
  178. )
  179. security = HTTPBearer()
  180. security_optional = HTTPBearer(auto_error=False)
  181. @app.get("/")
  182. async def root():
  183. """根路径"""
  184. return ApiResponse(
  185. code=0,
  186. message="欢迎使用SSO认证中心",
  187. data={
  188. "name": "SSO认证中心",
  189. "version": "1.0.0",
  190. "docs": "/docs"
  191. },
  192. timestamp=datetime.now(timezone.utc).isoformat()
  193. ).model_dump()
  194. @app.get("/health")
  195. async def health_check():
  196. """健康检查"""
  197. return ApiResponse(
  198. code=0,
  199. message="服务正常运行",
  200. data={
  201. "status": "healthy",
  202. "version": "1.0.0",
  203. "timestamp": datetime.now(timezone.utc).isoformat()
  204. },
  205. timestamp=datetime.now(timezone.utc).isoformat()
  206. ).model_dump()
  207. @app.post("/api/v1/auth/login")
  208. async def login(request: Request, login_data: LoginRequest):
  209. """用户登录"""
  210. print(f"🔐 收到登录请求: username={login_data.username}")
  211. conn = None
  212. cursor = None
  213. try:
  214. # 获取数据库连接
  215. print("📊 尝试连接数据库...")
  216. conn = get_db_connection()
  217. if not conn:
  218. print("❌ 数据库连接失败")
  219. return ApiResponse(
  220. code=500001,
  221. message="数据库连接失败",
  222. timestamp=datetime.now(timezone.utc).isoformat()
  223. ).model_dump()
  224. print("✅ 数据库连接成功")
  225. cursor = conn.cursor()
  226. # 查找用户
  227. print(f"🔍 查找用户: {login_data.username}")
  228. cursor.execute(
  229. "SELECT id, username, email, password_hash, is_active, is_superuser FROM users WHERE username = %s OR email = %s",
  230. (login_data.username, login_data.username)
  231. )
  232. user_data = cursor.fetchone()
  233. print(f"👤 用户查询结果: {user_data is not None}")
  234. if not user_data:
  235. print("❌ 用户不存在")
  236. return ApiResponse(
  237. code=200001,
  238. message="用户名或密码错误",
  239. timestamp=datetime.now(timezone.utc).isoformat()
  240. ).model_dump()
  241. user_id, username, email, password_hash, is_active, is_superuser = user_data
  242. print(f"✅ 找到用户: {username}, 激活状态: {is_active}")
  243. # 检查用户状态
  244. if not is_active:
  245. print("❌ 用户已被禁用")
  246. return ApiResponse(
  247. code=200002,
  248. message="用户已被禁用",
  249. timestamp=datetime.now(timezone.utc).isoformat()
  250. ).model_dump()
  251. # 验证密码
  252. print(f"🔑 验证密码,哈希格式: {password_hash[:20]}...")
  253. password_valid = verify_password_simple(login_data.password, password_hash)
  254. print(f"🔑 密码验证结果: {password_valid}")
  255. if not password_valid:
  256. print("❌ 密码验证失败")
  257. return ApiResponse(
  258. code=200001,
  259. message="用户名或密码错误",
  260. timestamp=datetime.now(timezone.utc).isoformat()
  261. ).model_dump()
  262. # 生成令牌
  263. print("🎫 生成访问令牌...")
  264. token_data = {
  265. "sub": user_id,
  266. "username": username,
  267. "email": email,
  268. "is_superuser": is_superuser
  269. }
  270. access_token = create_access_token(token_data)
  271. print(f"✅ 令牌生成成功: {access_token[:50]}...")
  272. token_response = TokenResponse(
  273. access_token=access_token,
  274. token_type="Bearer",
  275. expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  276. scope="profile email"
  277. )
  278. print("🎉 登录成功")
  279. return ApiResponse(
  280. code=0,
  281. message="登录成功",
  282. data=token_response.model_dump(),
  283. timestamp=datetime.now(timezone.utc).isoformat()
  284. ).model_dump()
  285. except Exception as e:
  286. print(f"❌ 登录错误详情: {type(e).__name__}: {str(e)}")
  287. import traceback
  288. print(f"❌ 错误堆栈: {traceback.format_exc()}")
  289. return ApiResponse(
  290. code=500001,
  291. message="服务器内部错误",
  292. timestamp=datetime.now(timezone.utc).isoformat()
  293. ).model_dump()
  294. finally:
  295. if cursor:
  296. cursor.close()
  297. if conn:
  298. conn.close()
  299. @app.get("/api/v1/users/profile")
  300. async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
  301. """获取用户资料"""
  302. try:
  303. # 验证令牌
  304. payload = verify_token(credentials.credentials)
  305. if not payload:
  306. return ApiResponse(
  307. code=200002,
  308. message="无效的访问令牌",
  309. timestamp=datetime.now(timezone.utc).isoformat()
  310. ).model_dump()
  311. user_id = payload.get("sub")
  312. if not user_id:
  313. return ApiResponse(
  314. code=200002,
  315. message="无效的访问令牌",
  316. timestamp=datetime.now(timezone.utc).isoformat()
  317. ).model_dump()
  318. # 获取数据库连接
  319. conn = get_db_connection()
  320. if not conn:
  321. return ApiResponse(
  322. code=500001,
  323. message="数据库连接失败",
  324. timestamp=datetime.now(timezone.utc).isoformat()
  325. ).model_dump()
  326. cursor = conn.cursor()
  327. # 查找用户详细信息
  328. cursor.execute("""
  329. SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active, u.is_superuser,
  330. u.last_login_at, u.created_at, u.updated_at,
  331. p.real_name, p.company, p.department, p.position
  332. FROM users u
  333. LEFT JOIN user_profiles p ON u.id = p.user_id
  334. WHERE u.id = %s
  335. """, (user_id,))
  336. user_data = cursor.fetchone()
  337. # 获取用户角色
  338. cursor.execute("""
  339. SELECT r.name
  340. FROM user_roles ur
  341. JOIN roles r ON ur.role_id = r.id
  342. WHERE ur.user_id = %s AND ur.is_active = 1
  343. """, (user_id,))
  344. roles = [row[0] for row in cursor.fetchall()]
  345. cursor.close()
  346. conn.close()
  347. if not user_data:
  348. return ApiResponse(
  349. code=200001,
  350. message="用户不存在",
  351. timestamp=datetime.now(timezone.utc).isoformat()
  352. ).model_dump()
  353. # 构建用户信息
  354. user_info = {
  355. "id": user_data[0],
  356. "username": user_data[1],
  357. "email": user_data[2],
  358. "phone": user_data[3],
  359. "avatar_url": user_data[4],
  360. "is_active": user_data[5],
  361. "is_superuser": user_data[6],
  362. "last_login_at": user_data[7].isoformat() if user_data[7] else None,
  363. "created_at": user_data[8].isoformat() if user_data[8] else None,
  364. "updated_at": user_data[9].isoformat() if user_data[9] else None,
  365. "real_name": user_data[10],
  366. "company": user_data[11],
  367. "department": user_data[12],
  368. "position": user_data[13],
  369. "roles": roles
  370. }
  371. return ApiResponse(
  372. code=0,
  373. message="获取用户资料成功",
  374. data=user_info,
  375. timestamp=datetime.now(timezone.utc).isoformat()
  376. ).model_dump()
  377. except Exception as e:
  378. print(f"获取用户资料错误: {e}")
  379. return ApiResponse(
  380. code=500001,
  381. message="服务器内部错误",
  382. timestamp=datetime.now(timezone.utc).isoformat()
  383. ).model_dump()
  384. @app.put("/api/v1/users/profile")
  385. async def update_user_profile(
  386. request: Request,
  387. profile_data: dict,
  388. credentials: HTTPAuthorizationCredentials = Depends(security)
  389. ):
  390. """更新用户资料"""
  391. try:
  392. # 验证令牌
  393. payload = verify_token(credentials.credentials)
  394. if not payload:
  395. return ApiResponse(
  396. code=200002,
  397. message="无效的访问令牌",
  398. timestamp=datetime.now(timezone.utc).isoformat()
  399. ).model_dump()
  400. user_id = payload.get("sub")
  401. # 获取数据库连接
  402. conn = get_db_connection()
  403. if not conn:
  404. return ApiResponse(
  405. code=500001,
  406. message="数据库连接失败",
  407. timestamp=datetime.now(timezone.utc).isoformat()
  408. ).model_dump()
  409. cursor = conn.cursor()
  410. # 更新用户基本信息
  411. update_fields = []
  412. update_values = []
  413. if 'email' in profile_data:
  414. update_fields.append('email = %s')
  415. update_values.append(profile_data['email'])
  416. if 'phone' in profile_data:
  417. update_fields.append('phone = %s')
  418. update_values.append(profile_data['phone'])
  419. if update_fields:
  420. update_values.append(user_id)
  421. cursor.execute(f"""
  422. UPDATE users
  423. SET {', '.join(update_fields)}, updated_at = NOW()
  424. WHERE id = %s
  425. """, update_values)
  426. # 更新或插入用户详情
  427. profile_fields = ['real_name', 'company', 'department', 'position']
  428. profile_updates = {k: v for k, v in profile_data.items() if k in profile_fields}
  429. if profile_updates:
  430. # 检查是否已有记录
  431. cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
  432. profile_exists = cursor.fetchone()
  433. if profile_exists:
  434. # 更新现有记录
  435. update_fields = []
  436. update_values = []
  437. for field, value in profile_updates.items():
  438. update_fields.append(f'{field} = %s')
  439. update_values.append(value)
  440. update_values.append(user_id)
  441. cursor.execute(f"""
  442. UPDATE user_profiles
  443. SET {', '.join(update_fields)}, updated_at = NOW()
  444. WHERE user_id = %s
  445. """, update_values)
  446. else:
  447. # 插入新记录
  448. fields = ['user_id'] + list(profile_updates.keys())
  449. values = [user_id] + list(profile_updates.values())
  450. placeholders = ', '.join(['%s'] * len(values))
  451. cursor.execute(f"""
  452. INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
  453. VALUES ({placeholders}, NOW(), NOW())
  454. """, values)
  455. conn.commit()
  456. cursor.close()
  457. conn.close()
  458. return ApiResponse(
  459. code=0,
  460. message="用户资料更新成功",
  461. timestamp=datetime.now(timezone.utc).isoformat()
  462. ).model_dump()
  463. except Exception as e:
  464. print(f"更新用户资料错误: {e}")
  465. return ApiResponse(
  466. code=500001,
  467. message="服务器内部错误",
  468. timestamp=datetime.now(timezone.utc).isoformat()
  469. ).model_dump()
  470. @app.put("/api/v1/users/password")
  471. async def change_user_password(
  472. request: Request,
  473. password_data: dict,
  474. credentials: HTTPAuthorizationCredentials = Depends(security)
  475. ):
  476. """修改用户密码"""
  477. try:
  478. # 验证令牌
  479. payload = verify_token(credentials.credentials)
  480. if not payload:
  481. return ApiResponse(
  482. code=200002,
  483. message="无效的访问令牌",
  484. timestamp=datetime.now(timezone.utc).isoformat()
  485. ).model_dump()
  486. user_id = payload.get("sub")
  487. old_password = password_data.get('old_password')
  488. new_password = password_data.get('new_password')
  489. if not old_password or not new_password:
  490. return ApiResponse(
  491. code=100001,
  492. message="缺少必要参数",
  493. timestamp=datetime.now(timezone.utc).isoformat()
  494. ).model_dump()
  495. # 获取数据库连接
  496. conn = get_db_connection()
  497. if not conn:
  498. return ApiResponse(
  499. code=500001,
  500. message="数据库连接失败",
  501. timestamp=datetime.now(timezone.utc).isoformat()
  502. ).model_dump()
  503. cursor = conn.cursor()
  504. # 验证当前密码
  505. cursor.execute("SELECT password_hash FROM users WHERE id = %s", (user_id,))
  506. result = cursor.fetchone()
  507. if not result or not verify_password_simple(old_password, result[0]):
  508. cursor.close()
  509. conn.close()
  510. return ApiResponse(
  511. code=200001,
  512. message="当前密码错误",
  513. timestamp=datetime.now(timezone.utc).isoformat()
  514. ).model_dump()
  515. # 生成新密码哈希
  516. new_password_hash = hash_password_simple(new_password)
  517. # 更新密码
  518. cursor.execute("""
  519. UPDATE users
  520. SET password_hash = %s, updated_at = NOW()
  521. WHERE id = %s
  522. """, (new_password_hash, user_id))
  523. conn.commit()
  524. cursor.close()
  525. conn.close()
  526. return ApiResponse(
  527. code=0,
  528. message="密码修改成功",
  529. timestamp=datetime.now(timezone.utc).isoformat()
  530. ).model_dump()
  531. except Exception as e:
  532. print(f"修改密码错误: {e}")
  533. return ApiResponse(
  534. code=500001,
  535. message="服务器内部错误",
  536. timestamp=datetime.now(timezone.utc).isoformat()
  537. ).model_dump()
  538. def hash_password_simple(password):
  539. """简单的密码哈希"""
  540. import hashlib
  541. import secrets
  542. # 生成盐值
  543. salt = secrets.token_hex(16)
  544. # 使用SHA256哈希
  545. password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
  546. return f"sha256${salt}${password_hash}"
  547. @app.post("/api/v1/auth/logout")
  548. async def logout():
  549. """用户登出"""
  550. return ApiResponse(
  551. code=0,
  552. message="登出成功",
  553. timestamp=datetime.now(timezone.utc).isoformat()
  554. ).model_dump()
  555. # OAuth2 授权端点
  556. @app.get("/oauth/authorize")
  557. async def oauth_authorize(
  558. response_type: str,
  559. client_id: str,
  560. redirect_uri: str,
  561. scope: str = "profile",
  562. state: str = None
  563. ):
  564. """OAuth2授权端点"""
  565. try:
  566. print(f"🔐 OAuth授权请求: client_id={client_id}, redirect_uri={redirect_uri}, scope={scope}")
  567. # 验证必要参数
  568. if not response_type or not client_id or not redirect_uri:
  569. error_url = f"{redirect_uri}?error=invalid_request&error_description=Missing required parameters"
  570. if state:
  571. error_url += f"&state={state}"
  572. return {"error": "invalid_request", "redirect_url": error_url}
  573. # 验证response_type
  574. if response_type != "code":
  575. error_url = f"{redirect_uri}?error=unsupported_response_type&error_description=Only authorization code flow is supported"
  576. if state:
  577. error_url += f"&state={state}"
  578. return {"error": "unsupported_response_type", "redirect_url": error_url}
  579. # 获取数据库连接
  580. conn = get_db_connection()
  581. if not conn:
  582. error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
  583. if state:
  584. error_url += f"&state={state}"
  585. return {"error": "server_error", "redirect_url": error_url}
  586. cursor = conn.cursor()
  587. # 验证client_id和redirect_uri
  588. cursor.execute("""
  589. SELECT id, name, redirect_uris, scope, is_active, is_trusted
  590. FROM apps
  591. WHERE app_key = %s AND is_active = 1
  592. """, (client_id,))
  593. app_data = cursor.fetchone()
  594. cursor.close()
  595. conn.close()
  596. if not app_data:
  597. error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client_id"
  598. if state:
  599. error_url += f"&state={state}"
  600. return {"error": "invalid_client", "redirect_url": error_url}
  601. app_id, app_name, redirect_uris_json, app_scope_json, is_active, is_trusted = app_data
  602. # 验证redirect_uri
  603. redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
  604. if redirect_uri not in redirect_uris:
  605. error_url = f"{redirect_uri}?error=invalid_request&error_description=Invalid redirect_uri"
  606. if state:
  607. error_url += f"&state={state}"
  608. return {"error": "invalid_request", "redirect_url": error_url}
  609. # 验证scope
  610. app_scopes = json.loads(app_scope_json) if app_scope_json else []
  611. requested_scopes = scope.split() if scope else []
  612. invalid_scopes = [s for s in requested_scopes if s not in app_scopes]
  613. if invalid_scopes:
  614. error_url = f"{redirect_uri}?error=invalid_scope&error_description=Invalid scope: {' '.join(invalid_scopes)}"
  615. if state:
  616. error_url += f"&state={state}"
  617. return {"error": "invalid_scope", "redirect_url": error_url}
  618. # TODO: 检查用户登录状态
  619. # 这里应该检查用户是否已登录(通过session或cookie)
  620. # 如果未登录,应该重定向到登录页面
  621. # 临时方案:返回登录页面,让用户先登录
  622. # 生产环境应该使用session管理
  623. # 构建登录页面URL,登录后返回授权页面
  624. login_page_url = f"/oauth/login?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}"
  625. if state:
  626. login_page_url += f"&state={state}"
  627. print(f"🔐 需要用户登录,重定向到登录页面: {login_page_url}")
  628. from fastapi.responses import RedirectResponse
  629. return RedirectResponse(url=login_page_url, status_code=302)
  630. # 非受信任应用需要用户授权确认
  631. # 这里返回授权页面HTML
  632. authorization_html = f"""
  633. <!DOCTYPE html>
  634. <html>
  635. <head>
  636. <title>授权确认 - SSO认证中心</title>
  637. <meta charset="utf-8">
  638. <meta name="viewport" content="width=device-width, initial-scale=1">
  639. <style>
  640. body {{
  641. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  642. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  643. margin: 0;
  644. padding: 20px;
  645. min-height: 100vh;
  646. display: flex;
  647. align-items: center;
  648. justify-content: center;
  649. }}
  650. .auth-container {{
  651. background: white;
  652. border-radius: 10px;
  653. padding: 40px;
  654. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  655. max-width: 400px;
  656. width: 100%;
  657. }}
  658. .auth-header {{
  659. text-align: center;
  660. margin-bottom: 30px;
  661. }}
  662. .auth-header h1 {{
  663. color: #333;
  664. margin-bottom: 10px;
  665. }}
  666. .app-info {{
  667. background: #f8f9fa;
  668. padding: 20px;
  669. border-radius: 8px;
  670. margin-bottom: 20px;
  671. }}
  672. .scope-list {{
  673. list-style: none;
  674. padding: 0;
  675. margin: 10px 0;
  676. }}
  677. .scope-list li {{
  678. padding: 5px 0;
  679. color: #666;
  680. }}
  681. .scope-list li:before {{
  682. content: "✓ ";
  683. color: #28a745;
  684. font-weight: bold;
  685. }}
  686. .auth-buttons {{
  687. display: flex;
  688. gap: 10px;
  689. margin-top: 20px;
  690. }}
  691. .btn {{
  692. flex: 1;
  693. padding: 12px 20px;
  694. border: none;
  695. border-radius: 6px;
  696. font-size: 16px;
  697. cursor: pointer;
  698. text-decoration: none;
  699. text-align: center;
  700. display: inline-block;
  701. }}
  702. .btn-primary {{
  703. background: #007bff;
  704. color: white;
  705. }}
  706. .btn-secondary {{
  707. background: #6c757d;
  708. color: white;
  709. }}
  710. .btn:hover {{
  711. opacity: 0.9;
  712. }}
  713. </style>
  714. </head>
  715. <body>
  716. <div class="auth-container">
  717. <div class="auth-header">
  718. <h1>授权确认</h1>
  719. <p>应用请求访问您的账户</p>
  720. </div>
  721. <div class="app-info">
  722. <h3>{app_name}</h3>
  723. <p>该应用请求以下权限:</p>
  724. <ul class="scope-list">
  725. """
  726. # 添加权限列表
  727. scope_descriptions = {
  728. "profile": "访问您的基本信息(用户名、头像等)",
  729. "email": "访问您的邮箱地址",
  730. "phone": "访问您的手机号码",
  731. "roles": "访问您的角色和权限信息"
  732. }
  733. for scope_item in requested_scopes:
  734. description = scope_descriptions.get(scope_item, f"访问 {scope_item} 信息")
  735. authorization_html += f"<li>{description}</li>"
  736. authorization_html += f"""
  737. </ul>
  738. </div>
  739. <div class="auth-buttons">
  740. <a href="/oauth/authorize/deny?client_id={client_id}&redirect_uri={redirect_uri}&state={state or ''}" class="btn btn-secondary">拒绝</a>
  741. <a href="/oauth/authorize/approve?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}" class="btn btn-primary">授权</a>
  742. </div>
  743. </div>
  744. </body>
  745. </html>
  746. """
  747. from fastapi.responses import HTMLResponse
  748. return HTMLResponse(content=authorization_html)
  749. except Exception as e:
  750. print(f"❌ OAuth授权错误: {e}")
  751. error_url = f"{redirect_uri}?error=server_error&error_description=Internal server error"
  752. if state:
  753. error_url += f"&state={state}"
  754. return {"error": "server_error", "redirect_url": error_url}
  755. @app.get("/oauth/login")
  756. async def oauth_login_page(
  757. response_type: str,
  758. client_id: str,
  759. redirect_uri: str,
  760. scope: str = "profile",
  761. state: str = None
  762. ):
  763. """OAuth2登录页面"""
  764. try:
  765. print(f"🔐 显示OAuth登录页面: client_id={client_id}")
  766. # 获取应用信息
  767. conn = get_db_connection()
  768. if not conn:
  769. return {"error": "server_error", "message": "数据库连接失败"}
  770. cursor = conn.cursor()
  771. cursor.execute("SELECT name FROM apps WHERE app_key = %s", (client_id,))
  772. app_data = cursor.fetchone()
  773. cursor.close()
  774. conn.close()
  775. app_name = app_data[0] if app_data else "未知应用"
  776. # 构建登录页面HTML
  777. login_html = f"""
  778. <!DOCTYPE html>
  779. <html lang="zh-CN">
  780. <head>
  781. <meta charset="UTF-8">
  782. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  783. <title>SSO登录 - {app_name}</title>
  784. <style>
  785. body {{
  786. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  787. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  788. margin: 0;
  789. padding: 20px;
  790. min-height: 100vh;
  791. display: flex;
  792. align-items: center;
  793. justify-content: center;
  794. }}
  795. .login-container {{
  796. background: white;
  797. border-radius: 15px;
  798. padding: 40px;
  799. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
  800. max-width: 400px;
  801. width: 100%;
  802. }}
  803. .login-header {{
  804. text-align: center;
  805. margin-bottom: 30px;
  806. }}
  807. .login-header h1 {{
  808. color: #333;
  809. margin-bottom: 10px;
  810. }}
  811. .app-info {{
  812. background: #f8f9fa;
  813. padding: 15px;
  814. border-radius: 8px;
  815. margin-bottom: 20px;
  816. text-align: center;
  817. }}
  818. .form-group {{
  819. margin-bottom: 20px;
  820. }}
  821. .form-group label {{
  822. display: block;
  823. margin-bottom: 5px;
  824. font-weight: 500;
  825. color: #333;
  826. }}
  827. .form-group input {{
  828. width: 100%;
  829. padding: 12px;
  830. border: 1px solid #ddd;
  831. border-radius: 6px;
  832. font-size: 16px;
  833. box-sizing: border-box;
  834. }}
  835. .form-group input:focus {{
  836. outline: none;
  837. border-color: #007bff;
  838. box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
  839. }}
  840. .btn {{
  841. width: 100%;
  842. padding: 12px;
  843. background: #007bff;
  844. color: white;
  845. border: none;
  846. border-radius: 6px;
  847. font-size: 16px;
  848. font-weight: 500;
  849. cursor: pointer;
  850. transition: background 0.3s;
  851. }}
  852. .btn:hover {{
  853. background: #0056b3;
  854. }}
  855. .btn:disabled {{
  856. background: #6c757d;
  857. cursor: not-allowed;
  858. }}
  859. .error-message {{
  860. color: #dc3545;
  861. font-size: 14px;
  862. margin-top: 10px;
  863. text-align: center;
  864. }}
  865. .success-message {{
  866. color: #28a745;
  867. font-size: 14px;
  868. margin-top: 10px;
  869. text-align: center;
  870. }}
  871. </style>
  872. </head>
  873. <body>
  874. <div class="login-container">
  875. <div class="login-header">
  876. <h1>🔐 SSO登录</h1>
  877. <p>请登录以继续访问应用</p>
  878. </div>
  879. <div class="app-info">
  880. <strong>{app_name}</strong> 请求访问您的账户
  881. </div>
  882. <form id="loginForm" onsubmit="handleLogin(event)">
  883. <div class="form-group">
  884. <label for="username">用户名或邮箱</label>
  885. <input type="text" id="username" name="username" required>
  886. </div>
  887. <div class="form-group">
  888. <label for="password">密码</label>
  889. <input type="password" id="password" name="password" required>
  890. </div>
  891. <button type="submit" class="btn" id="loginBtn">登录</button>
  892. <div id="message"></div>
  893. </form>
  894. <div style="margin-top: 20px; text-align: center; font-size: 14px; color: #666;">
  895. <p>测试账号: admin / Admin123456</p>
  896. </div>
  897. </div>
  898. <script>
  899. async function handleLogin(event) {{
  900. event.preventDefault();
  901. const loginBtn = document.getElementById('loginBtn');
  902. const messageDiv = document.getElementById('message');
  903. loginBtn.disabled = true;
  904. loginBtn.textContent = '登录中...';
  905. messageDiv.innerHTML = '';
  906. const formData = new FormData(event.target);
  907. const loginData = {{
  908. username: formData.get('username'),
  909. password: formData.get('password'),
  910. remember_me: false
  911. }};
  912. try {{
  913. // 调用登录API
  914. const response = await fetch('/api/v1/auth/login', {{
  915. method: 'POST',
  916. headers: {{
  917. 'Content-Type': 'application/json'
  918. }},
  919. body: JSON.stringify(loginData)
  920. }});
  921. const result = await response.json();
  922. if (result.code === 0) {{
  923. messageDiv.innerHTML = '<div class="success-message">登录成功,正在跳转...</div>';
  924. // 登录成功后,重定向到授权页面
  925. const authUrl = `/oauth/authorize/authenticated?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}&access_token=${{result.data.access_token}}`;
  926. setTimeout(() => {{
  927. window.location.href = authUrl;
  928. }}, 1000);
  929. }} else {{
  930. messageDiv.innerHTML = `<div class="error-message">${{result.message}}</div>`;
  931. }}
  932. }} catch (error) {{
  933. messageDiv.innerHTML = '<div class="error-message">登录失败,请重试</div>';
  934. }} finally {{
  935. loginBtn.disabled = false;
  936. loginBtn.textContent = '登录';
  937. }}
  938. }}
  939. </script>
  940. </body>
  941. </html>
  942. """
  943. from fastapi.responses import HTMLResponse
  944. return HTMLResponse(content=login_html)
  945. except Exception as e:
  946. print(f"❌ OAuth登录页面错误: {e}")
  947. return {"error": "server_error", "message": "服务器内部错误"}
  948. @app.get("/oauth/authorize/authenticated")
  949. async def oauth_authorize_authenticated(
  950. response_type: str,
  951. client_id: str,
  952. redirect_uri: str,
  953. access_token: str,
  954. scope: str = "profile",
  955. state: str = None
  956. ):
  957. """用户已登录后的授权处理"""
  958. try:
  959. print(f"🔐 用户已登录,处理授权: client_id={client_id}")
  960. # 验证访问令牌
  961. payload = verify_token(access_token)
  962. if not payload:
  963. error_url = f"{redirect_uri}?error=invalid_token&error_description=Invalid access token"
  964. if state:
  965. error_url += f"&state={state}"
  966. from fastapi.responses import RedirectResponse
  967. return RedirectResponse(url=error_url, status_code=302)
  968. user_id = payload.get("sub")
  969. username = payload.get("username", "")
  970. print(f"✅ 用户已验证: {username} ({user_id})")
  971. # 获取应用信息
  972. conn = get_db_connection()
  973. if not conn:
  974. error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
  975. if state:
  976. error_url += f"&state={state}"
  977. from fastapi.responses import RedirectResponse
  978. return RedirectResponse(url=error_url, status_code=302)
  979. cursor = conn.cursor()
  980. cursor.execute("SELECT name, is_trusted FROM apps WHERE app_key = %s", (client_id,))
  981. app_data = cursor.fetchone()
  982. cursor.close()
  983. conn.close()
  984. if not app_data:
  985. error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client"
  986. if state:
  987. error_url += f"&state={state}"
  988. from fastapi.responses import RedirectResponse
  989. return RedirectResponse(url=error_url, status_code=302)
  990. app_name, is_trusted = app_data
  991. # 如果是受信任应用,直接授权
  992. if is_trusted:
  993. # 生成授权码
  994. auth_code = secrets.token_urlsafe(32)
  995. # TODO: 将授权码存储到数据库,关联用户和应用
  996. # 这里简化处理,实际应该存储到数据库
  997. # 重定向回应用
  998. callback_url = f"{redirect_uri}?code={auth_code}"
  999. if state:
  1000. callback_url += f"&state={state}"
  1001. print(f"✅ 受信任应用自动授权: {callback_url}")
  1002. from fastapi.responses import RedirectResponse
  1003. return RedirectResponse(url=callback_url, status_code=302)
  1004. # 非受信任应用,显示授权确认页面
  1005. # 这里可以返回授权确认页面的HTML
  1006. # 为简化,暂时也直接授权
  1007. auth_code = secrets.token_urlsafe(32)
  1008. callback_url = f"{redirect_uri}?code={auth_code}"
  1009. if state:
  1010. callback_url += f"&state={state}"
  1011. print(f"✅ 用户授权完成: {callback_url}")
  1012. from fastapi.responses import RedirectResponse
  1013. return RedirectResponse(url=callback_url, status_code=302)
  1014. except Exception as e:
  1015. print(f"❌ 授权处理错误: {e}")
  1016. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1017. if state:
  1018. error_url += f"&state={state}"
  1019. from fastapi.responses import RedirectResponse
  1020. return RedirectResponse(url=error_url, status_code=302)
  1021. async def oauth_approve(
  1022. client_id: str,
  1023. redirect_uri: str,
  1024. scope: str = "profile",
  1025. state: str = None
  1026. ):
  1027. """用户同意授权"""
  1028. try:
  1029. print(f"✅ 用户同意授权: client_id={client_id}")
  1030. # 生成授权码
  1031. auth_code = secrets.token_urlsafe(32)
  1032. # TODO: 将授权码存储到数据库,关联用户和应用
  1033. # 这里简化处理,实际应该:
  1034. # 1. 验证用户登录状态
  1035. # 2. 将授权码存储到数据库
  1036. # 3. 设置过期时间(通常10分钟)
  1037. # 构建回调URL
  1038. callback_url = f"{redirect_uri}?code={auth_code}"
  1039. if state:
  1040. callback_url += f"&state={state}"
  1041. print(f"🔄 重定向到: {callback_url}")
  1042. from fastapi.responses import RedirectResponse
  1043. return RedirectResponse(url=callback_url, status_code=302)
  1044. except Exception as e:
  1045. print(f"❌ 授权确认错误: {e}")
  1046. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1047. if state:
  1048. error_url += f"&state={state}"
  1049. from fastapi.responses import RedirectResponse
  1050. return RedirectResponse(url=error_url, status_code=302)
  1051. @app.get("/oauth/authorize/deny")
  1052. async def oauth_deny(
  1053. client_id: str,
  1054. redirect_uri: str,
  1055. state: str = None
  1056. ):
  1057. """用户拒绝授权"""
  1058. try:
  1059. print(f"❌ 用户拒绝授权: client_id={client_id}")
  1060. # 构建错误回调URL
  1061. error_url = f"{redirect_uri}?error=access_denied&error_description=User denied authorization"
  1062. if state:
  1063. error_url += f"&state={state}"
  1064. from fastapi.responses import RedirectResponse
  1065. return RedirectResponse(url=error_url, status_code=302)
  1066. except Exception as e:
  1067. print(f"❌ 拒绝授权错误: {e}")
  1068. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1069. if state:
  1070. error_url += f"&state={state}"
  1071. from fastapi.responses import RedirectResponse
  1072. return RedirectResponse(url=error_url, status_code=302)
  1073. @app.post("/oauth/token")
  1074. async def oauth_token(request: Request):
  1075. """OAuth2令牌端点"""
  1076. try:
  1077. # 获取请求数据
  1078. form_data = await request.form()
  1079. grant_type = form_data.get("grant_type")
  1080. code = form_data.get("code")
  1081. redirect_uri = form_data.get("redirect_uri")
  1082. client_id = form_data.get("client_id")
  1083. client_secret = form_data.get("client_secret")
  1084. print(f"🎫 令牌请求: grant_type={grant_type}, client_id={client_id}")
  1085. # 验证grant_type
  1086. if grant_type != "authorization_code":
  1087. return {
  1088. "error": "unsupported_grant_type",
  1089. "error_description": "Only authorization_code grant type is supported"
  1090. }
  1091. # 验证必要参数
  1092. if not code or not redirect_uri or not client_id:
  1093. return {
  1094. "error": "invalid_request",
  1095. "error_description": "Missing required parameters"
  1096. }
  1097. # 获取数据库连接
  1098. conn = get_db_connection()
  1099. if not conn:
  1100. return {
  1101. "error": "server_error",
  1102. "error_description": "Database connection failed"
  1103. }
  1104. cursor = conn.cursor()
  1105. # 验证客户端
  1106. cursor.execute("""
  1107. SELECT id, name, app_secret, redirect_uris, scope, is_active
  1108. FROM apps
  1109. WHERE app_key = %s AND is_active = 1
  1110. """, (client_id,))
  1111. app_data = cursor.fetchone()
  1112. if not app_data:
  1113. cursor.close()
  1114. conn.close()
  1115. return {
  1116. "error": "invalid_client",
  1117. "error_description": "Invalid client credentials"
  1118. }
  1119. app_id, app_name, stored_secret, redirect_uris_json, scope_json, is_active = app_data
  1120. # 验证客户端密钥(如果提供了)
  1121. if client_secret and client_secret != stored_secret:
  1122. cursor.close()
  1123. conn.close()
  1124. return {
  1125. "error": "invalid_client",
  1126. "error_description": "Invalid client credentials"
  1127. }
  1128. # 验证redirect_uri
  1129. redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
  1130. if redirect_uri not in redirect_uris:
  1131. cursor.close()
  1132. conn.close()
  1133. return {
  1134. "error": "invalid_grant",
  1135. "error_description": "Invalid redirect_uri"
  1136. }
  1137. # TODO: 验证授权码
  1138. # 这里简化处理,实际应该:
  1139. # 1. 从数据库查找授权码
  1140. # 2. 验证授权码是否有效且未过期
  1141. # 3. 验证授权码是否已被使用
  1142. # 4. 获取关联的用户ID
  1143. # 模拟用户ID(实际应该从授权码记录中获取)
  1144. user_id = "ed6a79d3-0083-4d81-8b48-fc522f686f74" # admin用户ID
  1145. # 生成访问令牌
  1146. token_data = {
  1147. "sub": user_id,
  1148. "client_id": client_id,
  1149. "scope": "profile email"
  1150. }
  1151. access_token = create_access_token(token_data)
  1152. refresh_token = secrets.token_urlsafe(32)
  1153. # TODO: 将令牌存储到数据库
  1154. cursor.close()
  1155. conn.close()
  1156. # 返回令牌响应
  1157. token_response = {
  1158. "access_token": access_token,
  1159. "token_type": "Bearer",
  1160. "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  1161. "refresh_token": refresh_token,
  1162. "scope": "profile email"
  1163. }
  1164. print(f"✅ 令牌生成成功: {access_token[:50]}...")
  1165. return token_response
  1166. except Exception as e:
  1167. print(f"❌ 令牌生成错误: {e}")
  1168. return {
  1169. "error": "server_error",
  1170. "error_description": "Internal server error"
  1171. }
  1172. @app.get("/oauth/userinfo")
  1173. async def oauth_userinfo(credentials: HTTPAuthorizationCredentials = Depends(security)):
  1174. """OAuth2用户信息端点"""
  1175. try:
  1176. # 验证令牌
  1177. payload = verify_token(credentials.credentials)
  1178. if not payload:
  1179. return {
  1180. "error": "invalid_token",
  1181. "error_description": "Invalid or expired access token"
  1182. }
  1183. user_id = payload.get("sub")
  1184. client_id = payload.get("client_id")
  1185. scope = payload.get("scope", "").split()
  1186. print(f"👤 用户信息请求: user_id={user_id}, client_id={client_id}, scope={scope}")
  1187. # 获取数据库连接
  1188. conn = get_db_connection()
  1189. if not conn:
  1190. return {
  1191. "error": "server_error",
  1192. "error_description": "Database connection failed"
  1193. }
  1194. cursor = conn.cursor()
  1195. # 查找用户信息
  1196. cursor.execute("""
  1197. SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active,
  1198. p.real_name, p.company, p.department, p.position
  1199. FROM users u
  1200. LEFT JOIN user_profiles p ON u.id = p.user_id
  1201. WHERE u.id = %s AND u.is_active = 1
  1202. """, (user_id,))
  1203. user_data = cursor.fetchone()
  1204. cursor.close()
  1205. conn.close()
  1206. if not user_data:
  1207. return {
  1208. "error": "invalid_token",
  1209. "error_description": "User not found or inactive"
  1210. }
  1211. # 构建用户信息响应(根据scope过滤)
  1212. user_info = {"sub": user_data[0]}
  1213. if "profile" in scope:
  1214. user_info.update({
  1215. "username": user_data[1],
  1216. "avatar_url": user_data[4],
  1217. "real_name": user_data[6],
  1218. "company": user_data[7],
  1219. "department": user_data[8],
  1220. "position": user_data[9]
  1221. })
  1222. if "email" in scope:
  1223. user_info["email"] = user_data[2]
  1224. if "phone" in scope:
  1225. user_info["phone"] = user_data[3]
  1226. print(f"✅ 返回用户信息: {user_info}")
  1227. return user_info
  1228. except Exception as e:
  1229. print(f"❌ 获取用户信息错误: {e}")
  1230. return {
  1231. "error": "server_error",
  1232. "error_description": "Internal server error"
  1233. }
  1234. @app.get("/api/v1/apps")
  1235. async def get_apps(
  1236. page: int = 1,
  1237. page_size: int = 20,
  1238. keyword: str = "",
  1239. status: str = "",
  1240. credentials: HTTPAuthorizationCredentials = Depends(security)
  1241. ):
  1242. """获取应用列表"""
  1243. try:
  1244. # 验证令牌
  1245. payload = verify_token(credentials.credentials)
  1246. if not payload:
  1247. return ApiResponse(
  1248. code=200002,
  1249. message="无效的访问令牌",
  1250. timestamp=datetime.now(timezone.utc).isoformat()
  1251. ).model_dump()
  1252. user_id = payload.get("sub")
  1253. # 获取数据库连接
  1254. conn = get_db_connection()
  1255. if not conn:
  1256. return ApiResponse(
  1257. code=500001,
  1258. message="数据库连接失败",
  1259. timestamp=datetime.now(timezone.utc).isoformat()
  1260. ).model_dump()
  1261. cursor = conn.cursor()
  1262. # 检查用户角色,决定是否显示所有应用
  1263. cursor.execute("""
  1264. SELECT COUNT(*) FROM user_roles ur
  1265. JOIN roles r ON ur.role_id = r.id
  1266. WHERE ur.user_id = %s AND r.name IN ('super_admin', 'admin', 'app_manager') AND ur.is_active = 1
  1267. """, (user_id,))
  1268. is_app_manager = cursor.fetchone()[0] > 0
  1269. # 构建查询条件
  1270. where_conditions = []
  1271. params = []
  1272. # 如果不是应用管理员,只显示自己创建的应用
  1273. if not is_app_manager:
  1274. where_conditions.append("created_by = %s")
  1275. params.append(user_id)
  1276. if keyword:
  1277. where_conditions.append("(name LIKE %s OR description LIKE %s)")
  1278. params.extend([f"%{keyword}%", f"%{keyword}%"])
  1279. if status == "active":
  1280. where_conditions.append("is_active = 1")
  1281. elif status == "inactive":
  1282. where_conditions.append("is_active = 0")
  1283. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  1284. # 查询总数
  1285. cursor.execute(f"SELECT COUNT(*) FROM apps WHERE {where_clause}", params)
  1286. total = cursor.fetchone()[0]
  1287. # 查询应用列表
  1288. offset = (page - 1) * page_size
  1289. cursor.execute(f"""
  1290. SELECT id, name, app_key, description, icon_url, redirect_uris, scope,
  1291. is_active, is_trusted, access_token_expires, refresh_token_expires,
  1292. created_at, updated_at
  1293. FROM apps
  1294. WHERE {where_clause}
  1295. ORDER BY created_at DESC
  1296. LIMIT %s OFFSET %s
  1297. """, params + [page_size, offset])
  1298. apps = []
  1299. for row in cursor.fetchall():
  1300. app = {
  1301. "id": row[0],
  1302. "name": row[1],
  1303. "app_key": row[2],
  1304. "description": row[3],
  1305. "icon_url": row[4],
  1306. "redirect_uris": json.loads(row[5]) if row[5] else [],
  1307. "scope": json.loads(row[6]) if row[6] else [],
  1308. "is_active": bool(row[7]),
  1309. "is_trusted": bool(row[8]),
  1310. "access_token_expires": row[9],
  1311. "refresh_token_expires": row[10],
  1312. "created_at": row[11].isoformat() if row[11] else None,
  1313. "updated_at": row[12].isoformat() if row[12] else None,
  1314. # 模拟统计数据
  1315. "today_requests": secrets.randbelow(1000),
  1316. "active_users": secrets.randbelow(100)
  1317. }
  1318. apps.append(app)
  1319. cursor.close()
  1320. conn.close()
  1321. return ApiResponse(
  1322. code=0,
  1323. message="获取应用列表成功",
  1324. data={
  1325. "items": apps,
  1326. "total": total,
  1327. "page": page,
  1328. "page_size": page_size
  1329. },
  1330. timestamp=datetime.now(timezone.utc).isoformat()
  1331. ).model_dump()
  1332. except Exception as e:
  1333. print(f"获取应用列表错误: {e}")
  1334. return ApiResponse(
  1335. code=500001,
  1336. message="服务器内部错误",
  1337. timestamp=datetime.now(timezone.utc).isoformat()
  1338. ).model_dump()
  1339. @app.get("/api/v1/apps/{app_id}")
  1340. async def get_app_detail(
  1341. app_id: str,
  1342. credentials: HTTPAuthorizationCredentials = Depends(security)
  1343. ):
  1344. """获取应用详情(包含密钥)"""
  1345. try:
  1346. # 验证令牌
  1347. payload = verify_token(credentials.credentials)
  1348. if not payload:
  1349. return ApiResponse(
  1350. code=200002,
  1351. message="无效的访问令牌",
  1352. timestamp=datetime.now(timezone.utc).isoformat()
  1353. ).model_dump()
  1354. user_id = payload.get("sub")
  1355. # 获取数据库连接
  1356. conn = get_db_connection()
  1357. if not conn:
  1358. return ApiResponse(
  1359. code=500001,
  1360. message="数据库连接失败",
  1361. timestamp=datetime.now(timezone.utc).isoformat()
  1362. ).model_dump()
  1363. cursor = conn.cursor()
  1364. # 查询应用详情(包含密钥)
  1365. cursor.execute("""
  1366. SELECT id, name, app_key, app_secret, description, icon_url,
  1367. redirect_uris, scope, is_active, is_trusted,
  1368. access_token_expires, refresh_token_expires,
  1369. created_at, updated_at
  1370. FROM apps
  1371. WHERE id = %s AND created_by = %s
  1372. """, (app_id, user_id))
  1373. app_data = cursor.fetchone()
  1374. cursor.close()
  1375. conn.close()
  1376. if not app_data:
  1377. return ApiResponse(
  1378. code=200001,
  1379. message="应用不存在或无权限",
  1380. timestamp=datetime.now(timezone.utc).isoformat()
  1381. ).model_dump()
  1382. app_detail = {
  1383. "id": app_data[0],
  1384. "name": app_data[1],
  1385. "app_key": app_data[2],
  1386. "app_secret": app_data[3],
  1387. "description": app_data[4],
  1388. "icon_url": app_data[5],
  1389. "redirect_uris": json.loads(app_data[6]) if app_data[6] else [],
  1390. "scope": json.loads(app_data[7]) if app_data[7] else [],
  1391. "is_active": bool(app_data[8]),
  1392. "is_trusted": bool(app_data[9]),
  1393. "access_token_expires": app_data[10],
  1394. "refresh_token_expires": app_data[11],
  1395. "created_at": app_data[12].isoformat() if app_data[12] else None,
  1396. "updated_at": app_data[13].isoformat() if app_data[13] else None
  1397. }
  1398. return ApiResponse(
  1399. code=0,
  1400. message="获取应用详情成功",
  1401. data=app_detail,
  1402. timestamp=datetime.now(timezone.utc).isoformat()
  1403. ).model_dump()
  1404. except Exception as e:
  1405. print(f"获取应用详情错误: {e}")
  1406. return ApiResponse(
  1407. code=500001,
  1408. message="服务器内部错误",
  1409. timestamp=datetime.now(timezone.utc).isoformat()
  1410. ).model_dump()
  1411. @app.post("/api/v1/apps")
  1412. async def create_app(
  1413. request: Request,
  1414. app_data: dict,
  1415. credentials: HTTPAuthorizationCredentials = Depends(security)
  1416. ):
  1417. """创建应用"""
  1418. try:
  1419. # 验证令牌
  1420. payload = verify_token(credentials.credentials)
  1421. if not payload:
  1422. return ApiResponse(
  1423. code=200002,
  1424. message="无效的访问令牌",
  1425. timestamp=datetime.now(timezone.utc).isoformat()
  1426. ).model_dump()
  1427. user_id = payload.get("sub")
  1428. # 验证必要字段
  1429. if not app_data.get('name') or not app_data.get('redirect_uris'):
  1430. return ApiResponse(
  1431. code=100001,
  1432. message="缺少必要参数",
  1433. timestamp=datetime.now(timezone.utc).isoformat()
  1434. ).model_dump()
  1435. # 获取数据库连接
  1436. conn = get_db_connection()
  1437. if not conn:
  1438. return ApiResponse(
  1439. code=500001,
  1440. message="数据库连接失败",
  1441. timestamp=datetime.now(timezone.utc).isoformat()
  1442. ).model_dump()
  1443. cursor = conn.cursor()
  1444. # 生成应用ID和密钥
  1445. app_id = str(uuid.uuid4())
  1446. app_key = generate_random_string(32)
  1447. app_secret = generate_random_string(64)
  1448. # 插入应用记录
  1449. cursor.execute("""
  1450. INSERT INTO apps (
  1451. id, name, app_key, app_secret, description, icon_url,
  1452. redirect_uris, scope, is_active, is_trusted,
  1453. access_token_expires, refresh_token_expires, created_by,
  1454. created_at, updated_at
  1455. ) VALUES (
  1456. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
  1457. )
  1458. """, (
  1459. app_id,
  1460. app_data['name'],
  1461. app_key,
  1462. app_secret,
  1463. app_data.get('description', ''),
  1464. app_data.get('icon_url', ''),
  1465. json.dumps(app_data['redirect_uris']),
  1466. json.dumps(app_data.get('scope', ['profile'])),
  1467. True,
  1468. app_data.get('is_trusted', False),
  1469. app_data.get('access_token_expires', 7200),
  1470. app_data.get('refresh_token_expires', 2592000),
  1471. user_id
  1472. ))
  1473. conn.commit()
  1474. cursor.close()
  1475. conn.close()
  1476. # 返回创建的应用信息
  1477. app_info = {
  1478. "id": app_id,
  1479. "name": app_data['name'],
  1480. "app_key": app_key,
  1481. "app_secret": app_secret,
  1482. "description": app_data.get('description', ''),
  1483. "redirect_uris": app_data['redirect_uris'],
  1484. "scope": app_data.get('scope', ['profile']),
  1485. "is_active": True,
  1486. "is_trusted": app_data.get('is_trusted', False)
  1487. }
  1488. return ApiResponse(
  1489. code=0,
  1490. message="应用创建成功",
  1491. data=app_info,
  1492. timestamp=datetime.now(timezone.utc).isoformat()
  1493. ).model_dump()
  1494. except Exception as e:
  1495. print(f"创建应用错误: {e}")
  1496. return ApiResponse(
  1497. code=500001,
  1498. message="服务器内部错误",
  1499. timestamp=datetime.now(timezone.utc).isoformat()
  1500. ).model_dump()
  1501. @app.put("/api/v1/apps/{app_id}/status")
  1502. async def toggle_app_status(
  1503. app_id: str,
  1504. status_data: dict,
  1505. credentials: HTTPAuthorizationCredentials = Depends(security)
  1506. ):
  1507. """切换应用状态"""
  1508. try:
  1509. # 验证令牌
  1510. payload = verify_token(credentials.credentials)
  1511. if not payload:
  1512. return ApiResponse(
  1513. code=200002,
  1514. message="无效的访问令牌",
  1515. timestamp=datetime.now(timezone.utc).isoformat()
  1516. ).model_dump()
  1517. user_id = payload.get("sub")
  1518. is_active = status_data.get('is_active')
  1519. if is_active is None:
  1520. return ApiResponse(
  1521. code=100001,
  1522. message="缺少必要参数",
  1523. timestamp=datetime.now(timezone.utc).isoformat()
  1524. ).model_dump()
  1525. # 获取数据库连接
  1526. conn = get_db_connection()
  1527. if not conn:
  1528. return ApiResponse(
  1529. code=500001,
  1530. message="数据库连接失败",
  1531. timestamp=datetime.now(timezone.utc).isoformat()
  1532. ).model_dump()
  1533. cursor = conn.cursor()
  1534. # 检查应用是否存在且属于当前用户
  1535. cursor.execute("""
  1536. SELECT id, name FROM apps
  1537. WHERE id = %s AND created_by = %s
  1538. """, (app_id, user_id))
  1539. app_data = cursor.fetchone()
  1540. if not app_data:
  1541. cursor.close()
  1542. conn.close()
  1543. return ApiResponse(
  1544. code=200001,
  1545. message="应用不存在或无权限",
  1546. timestamp=datetime.now(timezone.utc).isoformat()
  1547. ).model_dump()
  1548. # 更新应用状态
  1549. cursor.execute("""
  1550. UPDATE apps
  1551. SET is_active = %s, updated_at = NOW()
  1552. WHERE id = %s
  1553. """, (is_active, app_id))
  1554. conn.commit()
  1555. cursor.close()
  1556. conn.close()
  1557. action = "启用" if is_active else "禁用"
  1558. print(f"✅ 应用状态已更新: {app_data[1]} -> {action}")
  1559. return ApiResponse(
  1560. code=0,
  1561. message=f"应用已{action}",
  1562. timestamp=datetime.now(timezone.utc).isoformat()
  1563. ).model_dump()
  1564. except Exception as e:
  1565. print(f"切换应用状态错误: {e}")
  1566. return ApiResponse(
  1567. code=500001,
  1568. message="服务器内部错误",
  1569. timestamp=datetime.now(timezone.utc).isoformat()
  1570. ).model_dump()
  1571. @app.put("/api/v1/apps/{app_id}")
  1572. async def update_app(
  1573. app_id: str,
  1574. app_data: dict,
  1575. credentials: HTTPAuthorizationCredentials = Depends(security)
  1576. ):
  1577. """更新应用信息"""
  1578. try:
  1579. # 验证令牌
  1580. payload = verify_token(credentials.credentials)
  1581. if not payload:
  1582. return ApiResponse(
  1583. code=200002,
  1584. message="无效的访问令牌",
  1585. timestamp=datetime.now(timezone.utc).isoformat()
  1586. ).model_dump()
  1587. user_id = payload.get("sub")
  1588. # 验证必要参数
  1589. name = app_data.get('name', '').strip()
  1590. if not name:
  1591. return ApiResponse(
  1592. code=100001,
  1593. message="应用名称不能为空",
  1594. timestamp=datetime.now(timezone.utc).isoformat()
  1595. ).model_dump()
  1596. # 获取数据库连接
  1597. conn = get_db_connection()
  1598. if not conn:
  1599. return ApiResponse(
  1600. code=500001,
  1601. message="数据库连接失败",
  1602. timestamp=datetime.now(timezone.utc).isoformat()
  1603. ).model_dump()
  1604. cursor = conn.cursor()
  1605. # 检查应用是否存在且属于当前用户
  1606. cursor.execute("""
  1607. SELECT id, name FROM apps
  1608. WHERE id = %s AND created_by = %s
  1609. """, (app_id, user_id))
  1610. existing_app = cursor.fetchone()
  1611. if not existing_app:
  1612. cursor.close()
  1613. conn.close()
  1614. return ApiResponse(
  1615. code=200001,
  1616. message="应用不存在或无权限",
  1617. timestamp=datetime.now(timezone.utc).isoformat()
  1618. ).model_dump()
  1619. # 检查应用名称是否已被其他应用使用
  1620. cursor.execute("""
  1621. SELECT id FROM apps
  1622. WHERE name = %s AND created_by = %s AND id != %s
  1623. """, (name, user_id, app_id))
  1624. if cursor.fetchone():
  1625. cursor.close()
  1626. conn.close()
  1627. return ApiResponse(
  1628. code=200001,
  1629. message="应用名称已存在",
  1630. timestamp=datetime.now(timezone.utc).isoformat()
  1631. ).model_dump()
  1632. # 准备更新数据
  1633. description = (app_data.get('description') or '').strip()
  1634. icon_url = (app_data.get('icon_url') or '').strip()
  1635. redirect_uris = app_data.get('redirect_uris', [])
  1636. scope = app_data.get('scope', ['profile', 'email'])
  1637. is_trusted = app_data.get('is_trusted', False)
  1638. access_token_expires = app_data.get('access_token_expires', 7200)
  1639. refresh_token_expires = app_data.get('refresh_token_expires', 2592000)
  1640. # 验证回调URL
  1641. if not redirect_uris or not isinstance(redirect_uris, list):
  1642. cursor.close()
  1643. conn.close()
  1644. return ApiResponse(
  1645. code=100001,
  1646. message="至少需要一个回调URL",
  1647. timestamp=datetime.now(timezone.utc).isoformat()
  1648. ).model_dump()
  1649. # 验证权限范围
  1650. if not scope or not isinstance(scope, list):
  1651. scope = ['profile', 'email']
  1652. # 更新应用信息
  1653. cursor.execute("""
  1654. UPDATE apps
  1655. SET name = %s, description = %s, icon_url = %s,
  1656. redirect_uris = %s, scope = %s, is_trusted = %s,
  1657. access_token_expires = %s, refresh_token_expires = %s,
  1658. updated_at = NOW()
  1659. WHERE id = %s
  1660. """, (
  1661. name, description, icon_url,
  1662. json.dumps(redirect_uris), json.dumps(scope), is_trusted,
  1663. access_token_expires, refresh_token_expires, app_id
  1664. ))
  1665. conn.commit()
  1666. # 获取更新后的应用信息
  1667. cursor.execute("""
  1668. SELECT id, name, app_key, description, icon_url,
  1669. redirect_uris, scope, is_active, is_trusted,
  1670. access_token_expires, refresh_token_expires,
  1671. created_at, updated_at
  1672. FROM apps
  1673. WHERE id = %s
  1674. """, (app_id,))
  1675. app_info = cursor.fetchone()
  1676. cursor.close()
  1677. conn.close()
  1678. if app_info:
  1679. app_result = {
  1680. "id": app_info[0],
  1681. "name": app_info[1],
  1682. "app_key": app_info[2],
  1683. "description": app_info[3],
  1684. "icon_url": app_info[4],
  1685. "redirect_uris": json.loads(app_info[5]) if app_info[5] else [],
  1686. "scope": json.loads(app_info[6]) if app_info[6] else [],
  1687. "is_active": bool(app_info[7]),
  1688. "is_trusted": bool(app_info[8]),
  1689. "access_token_expires": app_info[9],
  1690. "refresh_token_expires": app_info[10],
  1691. "created_at": app_info[11].isoformat() if app_info[11] else None,
  1692. "updated_at": app_info[12].isoformat() if app_info[12] else None
  1693. }
  1694. print(f"✅ 应用已更新: {name}")
  1695. return ApiResponse(
  1696. code=0,
  1697. message="应用更新成功",
  1698. data=app_result,
  1699. timestamp=datetime.now(timezone.utc).isoformat()
  1700. ).model_dump()
  1701. else:
  1702. return ApiResponse(
  1703. code=500001,
  1704. message="获取更新后的应用信息失败",
  1705. timestamp=datetime.now(timezone.utc).isoformat()
  1706. ).model_dump()
  1707. except Exception as e:
  1708. print(f"更新应用错误: {e}")
  1709. return ApiResponse(
  1710. code=500001,
  1711. message="服务器内部错误",
  1712. timestamp=datetime.now(timezone.utc).isoformat()
  1713. ).model_dump()
  1714. @app.delete("/api/v1/apps/{app_id}")
  1715. async def delete_app(
  1716. app_id: str,
  1717. credentials: HTTPAuthorizationCredentials = Depends(security)
  1718. ):
  1719. """删除应用"""
  1720. try:
  1721. # 验证令牌
  1722. payload = verify_token(credentials.credentials)
  1723. if not payload:
  1724. return ApiResponse(
  1725. code=200002,
  1726. message="无效的访问令牌",
  1727. timestamp=datetime.now(timezone.utc).isoformat()
  1728. ).model_dump()
  1729. user_id = payload.get("sub")
  1730. # 获取数据库连接
  1731. conn = get_db_connection()
  1732. if not conn:
  1733. return ApiResponse(
  1734. code=500001,
  1735. message="数据库连接失败",
  1736. timestamp=datetime.now(timezone.utc).isoformat()
  1737. ).model_dump()
  1738. cursor = conn.cursor()
  1739. # 检查应用是否存在且属于当前用户
  1740. cursor.execute("""
  1741. SELECT id, name FROM apps
  1742. WHERE id = %s AND created_by = %s
  1743. """, (app_id, user_id))
  1744. app_data = cursor.fetchone()
  1745. if not app_data:
  1746. cursor.close()
  1747. conn.close()
  1748. return ApiResponse(
  1749. code=200001,
  1750. message="应用不存在或无权限",
  1751. timestamp=datetime.now(timezone.utc).isoformat()
  1752. ).model_dump()
  1753. # 删除应用(级联删除相关数据)
  1754. cursor.execute("DELETE FROM apps WHERE id = %s", (app_id,))
  1755. conn.commit()
  1756. cursor.close()
  1757. conn.close()
  1758. print(f"✅ 应用已删除: {app_data[1]}")
  1759. return ApiResponse(
  1760. code=0,
  1761. message="应用已删除",
  1762. timestamp=datetime.now(timezone.utc).isoformat()
  1763. ).model_dump()
  1764. except Exception as e:
  1765. print(f"删除应用错误: {e}")
  1766. return ApiResponse(
  1767. code=500001,
  1768. message="服务器内部错误",
  1769. timestamp=datetime.now(timezone.utc).isoformat()
  1770. ).model_dump()
  1771. @app.post("/api/v1/apps/{app_id}/reset-secret")
  1772. async def reset_app_secret(
  1773. app_id: str,
  1774. credentials: HTTPAuthorizationCredentials = Depends(security)
  1775. ):
  1776. """重置应用密钥"""
  1777. try:
  1778. # 验证令牌
  1779. payload = verify_token(credentials.credentials)
  1780. if not payload:
  1781. return ApiResponse(
  1782. code=200002,
  1783. message="无效的访问令牌",
  1784. timestamp=datetime.now(timezone.utc).isoformat()
  1785. ).model_dump()
  1786. user_id = payload.get("sub")
  1787. # 获取数据库连接
  1788. conn = get_db_connection()
  1789. if not conn:
  1790. return ApiResponse(
  1791. code=500001,
  1792. message="数据库连接失败",
  1793. timestamp=datetime.now(timezone.utc).isoformat()
  1794. ).model_dump()
  1795. cursor = conn.cursor()
  1796. # 检查应用是否存在且属于当前用户
  1797. cursor.execute("""
  1798. SELECT id, name FROM apps
  1799. WHERE id = %s AND created_by = %s
  1800. """, (app_id, user_id))
  1801. app_data = cursor.fetchone()
  1802. if not app_data:
  1803. cursor.close()
  1804. conn.close()
  1805. return ApiResponse(
  1806. code=200001,
  1807. message="应用不存在或无权限",
  1808. timestamp=datetime.now(timezone.utc).isoformat()
  1809. ).model_dump()
  1810. # 生成新的应用密钥
  1811. new_secret = generate_random_string(64)
  1812. # 更新应用密钥
  1813. cursor.execute("""
  1814. UPDATE apps
  1815. SET app_secret = %s, updated_at = NOW()
  1816. WHERE id = %s
  1817. """, (new_secret, app_id))
  1818. conn.commit()
  1819. cursor.close()
  1820. conn.close()
  1821. print(f"✅ 应用密钥已重置: {app_data[1]}")
  1822. return ApiResponse(
  1823. code=0,
  1824. message="应用密钥已重置",
  1825. data={"app_secret": new_secret},
  1826. timestamp=datetime.now(timezone.utc).isoformat()
  1827. ).model_dump()
  1828. except Exception as e:
  1829. print(f"重置应用密钥错误: {e}")
  1830. return ApiResponse(
  1831. code=500001,
  1832. message="服务器内部错误",
  1833. timestamp=datetime.now(timezone.utc).isoformat()
  1834. ).model_dump()
  1835. def generate_random_string(length=32):
  1836. """生成随机字符串"""
  1837. import secrets
  1838. import string
  1839. alphabet = string.ascii_letters + string.digits
  1840. return ''.join(secrets.choice(alphabet) for _ in range(length))
  1841. """获取验证码"""
  1842. try:
  1843. # 生成验证码
  1844. captcha_text, captcha_image = generate_captcha()
  1845. # 这里应该将验证码文本存储到缓存中(Redis或内存)
  1846. # 为了简化,我们暂时返回固定的验证码
  1847. captcha_id = secrets.token_hex(16)
  1848. return ApiResponse(
  1849. code=0,
  1850. message="获取验证码成功",
  1851. data={
  1852. "captcha_id": captcha_id,
  1853. "captcha_image": captcha_image,
  1854. "captcha_text": captcha_text # 生产环境中不应该返回这个
  1855. },
  1856. timestamp=datetime.now(timezone.utc).isoformat()
  1857. ).model_dump()
  1858. except Exception as e:
  1859. print(f"生成验证码错误: {e}")
  1860. return ApiResponse(
  1861. code=500001,
  1862. message="生成验证码失败",
  1863. timestamp=datetime.now(timezone.utc).isoformat()
  1864. ).model_dump()
  1865. def generate_captcha():
  1866. """生成验证码"""
  1867. try:
  1868. from PIL import Image, ImageDraw, ImageFont
  1869. import io
  1870. import base64
  1871. import random
  1872. import string
  1873. # 生成随机验证码文本
  1874. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1875. # 创建图片
  1876. width, height = 120, 40
  1877. image = Image.new('RGB', (width, height), color='white')
  1878. draw = ImageDraw.Draw(image)
  1879. # 尝试使用系统字体,如果失败则使用默认字体
  1880. try:
  1881. # Windows系统字体
  1882. font = ImageFont.truetype("arial.ttf", 20)
  1883. except:
  1884. try:
  1885. # 备用字体
  1886. font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20)
  1887. except:
  1888. # 使用默认字体
  1889. font = ImageFont.load_default()
  1890. # 绘制验证码文本
  1891. text_width = draw.textlength(captcha_text, font=font)
  1892. text_height = 20
  1893. x = (width - text_width) // 2
  1894. y = (height - text_height) // 2
  1895. # 添加一些随机颜色
  1896. colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
  1897. text_color = random.choice(colors)
  1898. draw.text((x, y), captcha_text, fill=text_color, font=font)
  1899. # 添加一些干扰线
  1900. for _ in range(3):
  1901. x1 = random.randint(0, width)
  1902. y1 = random.randint(0, height)
  1903. x2 = random.randint(0, width)
  1904. y2 = random.randint(0, height)
  1905. draw.line([(x1, y1), (x2, y2)], fill=random.choice(colors), width=1)
  1906. # 添加一些干扰点
  1907. for _ in range(20):
  1908. x = random.randint(0, width)
  1909. y = random.randint(0, height)
  1910. draw.point((x, y), fill=random.choice(colors))
  1911. # 转换为base64
  1912. buffer = io.BytesIO()
  1913. image.save(buffer, format='PNG')
  1914. image_data = buffer.getvalue()
  1915. image_base64 = base64.b64encode(image_data).decode('utf-8')
  1916. return captcha_text, f"data:image/png;base64,{image_base64}"
  1917. except ImportError:
  1918. # 如果PIL不可用,返回简单的文本验证码
  1919. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1920. # 创建一个简单的SVG验证码
  1921. svg_captcha = f"""
  1922. <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
  1923. <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
  1924. <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
  1925. </svg>
  1926. """
  1927. svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
  1928. return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
  1929. except Exception as e:
  1930. print(f"生成验证码图片失败: {e}")
  1931. # 返回默认验证码
  1932. return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="
  1933. # RBAC权限管理API
  1934. @app.get("/api/v1/user/menus")
  1935. async def api_get_user_menus(credentials: HTTPAuthorizationCredentials = Depends(security)):
  1936. """获取用户菜单"""
  1937. try:
  1938. payload = verify_token(credentials.credentials)
  1939. if not payload:
  1940. return ApiResponse(
  1941. code=401,
  1942. message="无效的访问令牌",
  1943. timestamp=datetime.now(timezone.utc).isoformat()
  1944. ).model_dump()
  1945. user_id = payload.get("sub")
  1946. conn = get_db_connection()
  1947. if not conn:
  1948. return ApiResponse(
  1949. code=500,
  1950. message="数据库连接失败",
  1951. timestamp=datetime.now(timezone.utc).isoformat()
  1952. ).model_dump()
  1953. cursor = conn.cursor()
  1954. # 检查用户是否是超级管理员
  1955. cursor.execute("""
  1956. SELECT COUNT(*) FROM user_roles ur
  1957. JOIN roles r ON ur.role_id = r.id
  1958. WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
  1959. """, (user_id,))
  1960. is_super_admin = cursor.fetchone()[0] > 0
  1961. if is_super_admin:
  1962. # 超级管理员返回所有活跃菜单
  1963. cursor.execute("""
  1964. SELECT m.id, m.parent_id, m.name, m.title, m.path,
  1965. m.component, m.icon, m.sort_order, m.menu_type,
  1966. m.is_hidden, m.is_active
  1967. FROM menus m
  1968. WHERE m.is_active = 1
  1969. ORDER BY m.sort_order
  1970. """)
  1971. else:
  1972. # 普通用户根据角色权限获取菜单
  1973. cursor.execute("""
  1974. SELECT m.id, m.parent_id, m.name, m.title, m.path,
  1975. m.component, m.icon, m.sort_order, m.menu_type,
  1976. m.is_hidden, m.is_active
  1977. FROM menus m
  1978. JOIN role_menus rm ON m.id = rm.menu_id
  1979. JOIN user_roles ur ON rm.role_id = ur.role_id
  1980. WHERE ur.user_id = %s
  1981. AND ur.is_active = 1
  1982. AND m.is_active = 1
  1983. GROUP BY m.id, m.parent_id, m.name, m.title, m.path,
  1984. m.component, m.icon, m.sort_order, m.menu_type,
  1985. m.is_hidden, m.is_active
  1986. ORDER BY m.sort_order
  1987. """, (user_id,))
  1988. menus = []
  1989. for row in cursor.fetchall():
  1990. menu_id = str(row[0])
  1991. menu_name = str(row[2])
  1992. menu_title = str(row[3])
  1993. menu_path = str(row[4])
  1994. # 只过滤掉明确不想要的“文档处理中心”
  1995. # 保留数据库中原本就有的“文档管理中心” (/admin/documents)
  1996. if "文档处理中心" in menu_title:
  1997. continue
  1998. menu = {
  1999. "id": row[0],
  2000. "parent_id": row[1],
  2001. "name": row[2],
  2002. "title": row[3],
  2003. "path": row[4],
  2004. "component": row[5],
  2005. "icon": row[6],
  2006. "sort_order": row[7],
  2007. "menu_type": row[8],
  2008. "is_hidden": bool(row[9]),
  2009. "is_active": bool(row[10]),
  2010. "children": []
  2011. }
  2012. menus.append(menu)
  2013. # 构建菜单树前,过滤掉 button 类型的项,侧边栏只显示 menu 类型
  2014. sidebar_menus = [m for m in menus if m.get("menu_type") == "menu"]
  2015. menu_tree = build_menu_tree(sidebar_menus)
  2016. cursor.close()
  2017. conn.close()
  2018. return ApiResponse(
  2019. code=0,
  2020. message="获取用户菜单成功",
  2021. data=menu_tree,
  2022. timestamp=datetime.now(timezone.utc).isoformat()
  2023. ).model_dump()
  2024. except Exception as e:
  2025. print(f"获取用户菜单错误: {e}")
  2026. return ApiResponse(
  2027. code=500,
  2028. message="服务器内部错误",
  2029. timestamp=datetime.now(timezone.utc).isoformat()
  2030. ).model_dump()
  2031. def build_menu_tree(menus):
  2032. """构建菜单树结构"""
  2033. menu_map = {menu["id"]: menu for menu in menus}
  2034. tree = []
  2035. for menu in menus:
  2036. if menu["parent_id"] is None:
  2037. tree.append(menu)
  2038. else:
  2039. parent = menu_map.get(menu["parent_id"])
  2040. if parent:
  2041. parent["children"].append(menu)
  2042. return tree
  2043. @app.get("/api/v1/admin/menus")
  2044. async def api_get_all_menus(
  2045. page: int = 1,
  2046. page_size: int = 1000, # 增大默认页面大小,确保返回所有菜单
  2047. keyword: Optional[str] = None,
  2048. credentials: HTTPAuthorizationCredentials = Depends(security)
  2049. ):
  2050. """获取所有菜单(管理员)"""
  2051. try:
  2052. payload = verify_token(credentials.credentials)
  2053. if not payload:
  2054. return ApiResponse(
  2055. code=401,
  2056. message="无效的访问令牌",
  2057. timestamp=datetime.now(timezone.utc).isoformat()
  2058. ).model_dump()
  2059. # 简化权限检查 - 只检查是否为管理员
  2060. is_superuser = payload.get("is_superuser", False)
  2061. if not is_superuser:
  2062. return ApiResponse(
  2063. code=403,
  2064. message="权限不足",
  2065. timestamp=datetime.now(timezone.utc).isoformat()
  2066. ).model_dump()
  2067. conn = get_db_connection()
  2068. if not conn:
  2069. return ApiResponse(
  2070. code=500,
  2071. message="数据库连接失败",
  2072. timestamp=datetime.now(timezone.utc).isoformat()
  2073. ).model_dump()
  2074. cursor = conn.cursor()
  2075. # 构建查询条件
  2076. where_conditions = []
  2077. params = []
  2078. if keyword:
  2079. where_conditions.append("(m.title LIKE %s OR m.name LIKE %s)")
  2080. params.extend([f"%{keyword}%", f"%{keyword}%"])
  2081. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  2082. # 查询总数
  2083. cursor.execute(f"SELECT COUNT(*) FROM menus m WHERE {where_clause}", params)
  2084. total = cursor.fetchone()[0]
  2085. # 查询菜单列表 - 修改排序逻辑以支持树形结构
  2086. cursor.execute(f"""
  2087. SELECT m.id, m.parent_id, m.name, m.title, m.path, m.component,
  2088. m.icon, m.sort_order, m.menu_type, m.is_hidden, m.is_active,
  2089. m.description, m.created_at, m.updated_at,
  2090. pm.title as parent_title
  2091. FROM menus m
  2092. LEFT JOIN menus pm ON m.parent_id = pm.id
  2093. WHERE {where_clause}
  2094. ORDER BY
  2095. CASE WHEN m.parent_id IS NULL THEN 0 ELSE 1 END,
  2096. m.sort_order,
  2097. CASE WHEN m.menu_type = 'menu' THEN 0 ELSE 1 END,
  2098. m.created_at
  2099. LIMIT %s OFFSET %s
  2100. """, params + [page_size, (page - 1) * page_size])
  2101. menus = []
  2102. for row in cursor.fetchall():
  2103. menu = {
  2104. "id": row[0],
  2105. "parent_id": row[1],
  2106. "name": row[2],
  2107. "title": row[3],
  2108. "path": row[4],
  2109. "component": row[5],
  2110. "icon": row[6],
  2111. "sort_order": row[7],
  2112. "menu_type": row[8],
  2113. "is_hidden": bool(row[9]),
  2114. "is_active": bool(row[10]),
  2115. "description": row[11],
  2116. "created_at": row[12].isoformat() if row[12] else None,
  2117. "updated_at": row[13].isoformat() if row[13] else None,
  2118. "parent_title": row[14]
  2119. }
  2120. menus.append(menu)
  2121. cursor.close()
  2122. conn.close()
  2123. return ApiResponse(
  2124. code=0,
  2125. message="获取菜单列表成功",
  2126. data={
  2127. "items": menus,
  2128. "total": total,
  2129. "page": page,
  2130. "page_size": page_size
  2131. },
  2132. timestamp=datetime.now(timezone.utc).isoformat()
  2133. ).model_dump()
  2134. except Exception as e:
  2135. print(f"获取菜单列表错误: {e}")
  2136. return ApiResponse(
  2137. code=500,
  2138. message="服务器内部错误",
  2139. timestamp=datetime.now(timezone.utc).isoformat()
  2140. ).model_dump()
  2141. @app.get("/api/v1/admin/roles")
  2142. async def api_get_all_roles(
  2143. page: int = 1,
  2144. page_size: int = 20,
  2145. keyword: Optional[str] = None,
  2146. credentials: HTTPAuthorizationCredentials = Depends(security)
  2147. ):
  2148. """获取所有角色"""
  2149. try:
  2150. payload = verify_token(credentials.credentials)
  2151. if not payload:
  2152. return ApiResponse(
  2153. code=401,
  2154. message="无效的访问令牌",
  2155. timestamp=datetime.now(timezone.utc).isoformat()
  2156. ).model_dump()
  2157. # 简化权限检查 - 只检查是否为管理员
  2158. is_superuser = payload.get("is_superuser", False)
  2159. if not is_superuser:
  2160. return ApiResponse(
  2161. code=403,
  2162. message="权限不足",
  2163. timestamp=datetime.now(timezone.utc).isoformat()
  2164. ).model_dump()
  2165. conn = get_db_connection()
  2166. if not conn:
  2167. return ApiResponse(
  2168. code=500,
  2169. message="数据库连接失败",
  2170. timestamp=datetime.now(timezone.utc).isoformat()
  2171. ).model_dump()
  2172. cursor = conn.cursor()
  2173. # 构建查询条件
  2174. where_conditions = []
  2175. params = []
  2176. if keyword:
  2177. where_conditions.append("(r.display_name LIKE %s OR r.name LIKE %s)")
  2178. params.extend([f"%{keyword}%", f"%{keyword}%"])
  2179. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  2180. # 查询总数
  2181. cursor.execute(f"SELECT COUNT(*) FROM roles r WHERE {where_clause}", params)
  2182. total = cursor.fetchone()[0]
  2183. # 查询角色列表
  2184. offset = (page - 1) * page_size
  2185. cursor.execute(f"""
  2186. SELECT r.id, r.name, r.display_name, r.description, r.is_active,
  2187. r.is_system, r.created_at, r.updated_at,
  2188. COUNT(ur.user_id) as user_count
  2189. FROM roles r
  2190. LEFT JOIN user_roles ur ON r.id = ur.role_id AND ur.is_active = 1
  2191. WHERE {where_clause}
  2192. GROUP BY r.id
  2193. ORDER BY r.is_system DESC, r.created_at
  2194. LIMIT %s OFFSET %s
  2195. """, params + [page_size, offset])
  2196. roles = []
  2197. for row in cursor.fetchall():
  2198. role = {
  2199. "id": row[0],
  2200. "name": row[1],
  2201. "display_name": row[2],
  2202. "description": row[3],
  2203. "is_active": bool(row[4]),
  2204. "is_system": bool(row[5]),
  2205. "created_at": row[6].isoformat() if row[6] else None,
  2206. "updated_at": row[7].isoformat() if row[7] else None,
  2207. "user_count": row[8]
  2208. }
  2209. roles.append(role)
  2210. cursor.close()
  2211. conn.close()
  2212. return ApiResponse(
  2213. code=0,
  2214. message="获取角色列表成功",
  2215. data={
  2216. "items": roles,
  2217. "total": total,
  2218. "page": page,
  2219. "page_size": page_size
  2220. },
  2221. timestamp=datetime.now(timezone.utc).isoformat()
  2222. ).model_dump()
  2223. except Exception as e:
  2224. print(f"获取角色列表错误: {e}")
  2225. return ApiResponse(
  2226. code=500,
  2227. message="服务器内部错误",
  2228. timestamp=datetime.now(timezone.utc).isoformat()
  2229. ).model_dump()
  2230. @app.get("/api/v1/user/permissions")
  2231. async def api_get_user_permissions(credentials: HTTPAuthorizationCredentials = Depends(security)):
  2232. """获取用户权限"""
  2233. try:
  2234. payload = verify_token(credentials.credentials)
  2235. if not payload:
  2236. return ApiResponse(
  2237. code=401,
  2238. message="无效的访问令牌",
  2239. timestamp=datetime.now(timezone.utc).isoformat()
  2240. ).model_dump()
  2241. user_id = payload.get("sub")
  2242. conn = get_db_connection()
  2243. if not conn:
  2244. return ApiResponse(
  2245. code=500,
  2246. message="数据库连接失败",
  2247. timestamp=datetime.now(timezone.utc).isoformat()
  2248. ).model_dump()
  2249. cursor = conn.cursor()
  2250. # 获取用户权限
  2251. cursor.execute("""
  2252. SELECT DISTINCT p.name, p.resource, p.action
  2253. FROM permissions p
  2254. JOIN role_permissions rp ON p.id = rp.permission_id
  2255. JOIN user_roles ur ON rp.role_id = ur.role_id
  2256. WHERE ur.user_id = %s
  2257. AND ur.is_active = 1
  2258. AND p.is_active = 1
  2259. """, (user_id,))
  2260. permissions = []
  2261. for row in cursor.fetchall():
  2262. permissions.append({
  2263. "name": row[0],
  2264. "resource": row[1],
  2265. "action": row[2]
  2266. })
  2267. cursor.close()
  2268. conn.close()
  2269. return ApiResponse(
  2270. code=0,
  2271. message="获取用户权限成功",
  2272. data=permissions,
  2273. timestamp=datetime.now(timezone.utc).isoformat()
  2274. ).model_dump()
  2275. except Exception as e:
  2276. print(f"获取用户权限错误: {e}")
  2277. return ApiResponse(
  2278. code=500,
  2279. message="服务器内部错误",
  2280. timestamp=datetime.now(timezone.utc).isoformat()
  2281. ).model_dump()
  2282. # 用户管理API
  2283. @app.get("/api/v1/admin/users")
  2284. async def get_users(
  2285. page: int = 1,
  2286. page_size: int = 20,
  2287. keyword: Optional[str] = None,
  2288. credentials: HTTPAuthorizationCredentials = Depends(security)
  2289. ):
  2290. """获取用户列表"""
  2291. try:
  2292. payload = verify_token(credentials.credentials)
  2293. if not payload:
  2294. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2295. is_superuser = payload.get("is_superuser", False)
  2296. if not is_superuser:
  2297. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2298. conn = get_db_connection()
  2299. if not conn:
  2300. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2301. cursor = conn.cursor()
  2302. # 构建查询条件
  2303. where_conditions = []
  2304. params = []
  2305. if keyword:
  2306. where_conditions.append("(u.username LIKE %s OR u.email LIKE %s OR up.real_name LIKE %s)")
  2307. params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
  2308. where_clause = " AND ".join(where_conditions) if where_conditions else "1=1"
  2309. # 查询总数
  2310. cursor.execute(f"SELECT COUNT(*) FROM users u LEFT JOIN user_profiles up ON u.id = up.user_id WHERE {where_clause}", params)
  2311. total = cursor.fetchone()[0]
  2312. # 查询用户列表
  2313. offset = (page - 1) * page_size
  2314. cursor.execute(f"""
  2315. SELECT u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
  2316. u.last_login_at, u.created_at, up.real_name, up.company, up.department,
  2317. GROUP_CONCAT(r.display_name) as roles
  2318. FROM users u
  2319. LEFT JOIN user_profiles up ON u.id = up.user_id
  2320. LEFT JOIN user_roles ur ON u.id = ur.user_id AND ur.is_active = 1
  2321. LEFT JOIN roles r ON ur.role_id = r.id
  2322. WHERE {where_clause}
  2323. GROUP BY u.id, u.username, u.email, u.phone, u.is_active, u.is_superuser,
  2324. u.last_login_at, u.created_at, up.real_name, up.company, up.department
  2325. ORDER BY u.created_at DESC
  2326. LIMIT %s OFFSET %s
  2327. """, params + [page_size, offset])
  2328. users = []
  2329. for row in cursor.fetchall():
  2330. users.append({
  2331. "id": row[0],
  2332. "username": row[1],
  2333. "email": row[2],
  2334. "phone": row[3],
  2335. "is_active": bool(row[4]),
  2336. "is_superuser": bool(row[5]),
  2337. "last_login_at": row[6].isoformat() if row[6] else None,
  2338. "created_at": row[7].isoformat() if row[7] else None,
  2339. "real_name": row[8],
  2340. "company": row[9],
  2341. "department": row[10],
  2342. "roles": row[11].split(',') if row[11] else []
  2343. })
  2344. cursor.close()
  2345. conn.close()
  2346. return ApiResponse(
  2347. code=0,
  2348. message="获取用户列表成功",
  2349. data={"items": users, "total": total, "page": page, "page_size": page_size},
  2350. timestamp=datetime.now(timezone.utc).isoformat()
  2351. ).model_dump()
  2352. except Exception as e:
  2353. print(f"获取用户列表错误: {e}")
  2354. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2355. @app.post("/api/v1/admin/users")
  2356. async def create_user(
  2357. user_data: dict,
  2358. credentials: HTTPAuthorizationCredentials = Depends(security)
  2359. ):
  2360. """创建用户"""
  2361. try:
  2362. payload = verify_token(credentials.credentials)
  2363. if not payload:
  2364. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2365. is_superuser = payload.get("is_superuser", False)
  2366. if not is_superuser:
  2367. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2368. conn = get_db_connection()
  2369. if not conn:
  2370. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2371. cursor = conn.cursor()
  2372. # 检查用户名和邮箱是否已存在
  2373. cursor.execute("SELECT id FROM users WHERE username = %s OR email = %s",
  2374. (user_data['username'], user_data['email']))
  2375. if cursor.fetchone():
  2376. cursor.close()
  2377. conn.close()
  2378. return ApiResponse(code=400, message="用户名或邮箱已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2379. # 生成用户ID
  2380. user_id = str(uuid.uuid4())
  2381. # 创建密码哈希
  2382. password_hash = hash_password_simple(user_data['password'])
  2383. # 插入用户
  2384. cursor.execute("""
  2385. INSERT INTO users (id, username, email, phone, password_hash, is_active, is_superuser, created_at, updated_at)
  2386. VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
  2387. """, (user_id, user_data['username'], user_data['email'], user_data.get('phone'),
  2388. password_hash, user_data.get('is_active', True), user_data.get('is_superuser', False)))
  2389. # 插入用户详情
  2390. if any(key in user_data for key in ['real_name', 'company', 'department']):
  2391. profile_id = str(uuid.uuid4())
  2392. cursor.execute("""
  2393. INSERT INTO user_profiles (id, user_id, real_name, company, department, created_at, updated_at)
  2394. VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
  2395. """, (profile_id, user_id, user_data.get('real_name'), user_data.get('company'), user_data.get('department')))
  2396. # 分配角色
  2397. if 'role_ids' in user_data and user_data['role_ids']:
  2398. for role_id in user_data['role_ids']:
  2399. role_assignment_id = str(uuid.uuid4())
  2400. cursor.execute("""
  2401. INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
  2402. VALUES (%s, %s, %s, %s, NOW())
  2403. """, (role_assignment_id, user_id, role_id, payload.get("sub")))
  2404. conn.commit()
  2405. cursor.close()
  2406. conn.close()
  2407. return ApiResponse(code=0, message="用户创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2408. except Exception as e:
  2409. print(f"创建用户错误: {e}")
  2410. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2411. @app.put("/api/v1/admin/users/{user_id}")
  2412. async def update_user(
  2413. user_id: str,
  2414. user_data: dict,
  2415. credentials: HTTPAuthorizationCredentials = Depends(security)
  2416. ):
  2417. """更新用户"""
  2418. try:
  2419. payload = verify_token(credentials.credentials)
  2420. if not payload:
  2421. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2422. is_superuser = payload.get("is_superuser", False)
  2423. if not is_superuser:
  2424. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2425. conn = get_db_connection()
  2426. if not conn:
  2427. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2428. cursor = conn.cursor()
  2429. # 更新用户基本信息
  2430. update_fields = []
  2431. update_values = []
  2432. for field in ['email', 'phone', 'is_active', 'is_superuser']:
  2433. if field in user_data:
  2434. update_fields.append(f'{field} = %s')
  2435. update_values.append(user_data[field])
  2436. if update_fields:
  2437. update_values.append(user_id)
  2438. cursor.execute(f"""
  2439. UPDATE users
  2440. SET {', '.join(update_fields)}, updated_at = NOW()
  2441. WHERE id = %s
  2442. """, update_values)
  2443. # 更新用户详情
  2444. profile_fields = ['real_name', 'company', 'department']
  2445. profile_updates = {k: v for k, v in user_data.items() if k in profile_fields}
  2446. if profile_updates:
  2447. # 检查是否已有记录
  2448. cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
  2449. profile_exists = cursor.fetchone()
  2450. if profile_exists:
  2451. update_fields = []
  2452. update_values = []
  2453. for field, value in profile_updates.items():
  2454. update_fields.append(f'{field} = %s')
  2455. update_values.append(value)
  2456. update_values.append(user_id)
  2457. cursor.execute(f"""
  2458. UPDATE user_profiles
  2459. SET {', '.join(update_fields)}, updated_at = NOW()
  2460. WHERE user_id = %s
  2461. """, update_values)
  2462. else:
  2463. profile_id = str(uuid.uuid4())
  2464. fields = ['id', 'user_id'] + list(profile_updates.keys())
  2465. values = [profile_id, user_id] + list(profile_updates.values())
  2466. placeholders = ', '.join(['%s'] * len(values))
  2467. cursor.execute(f"""
  2468. INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
  2469. VALUES ({placeholders}, NOW(), NOW())
  2470. """, values)
  2471. # 更新用户角色
  2472. if 'role_ids' in user_data:
  2473. # 删除现有角色
  2474. cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
  2475. # 添加新角色
  2476. for role_id in user_data['role_ids']:
  2477. assignment_id = str(uuid.uuid4())
  2478. cursor.execute("""
  2479. INSERT INTO user_roles (id, user_id, role_id, assigned_by, created_at)
  2480. VALUES (%s, %s, %s, %s, NOW())
  2481. """, (assignment_id, user_id, role_id, payload.get("sub")))
  2482. conn.commit()
  2483. cursor.close()
  2484. conn.close()
  2485. return ApiResponse(code=0, message="用户更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2486. except Exception as e:
  2487. print(f"更新用户错误: {e}")
  2488. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2489. @app.delete("/api/v1/admin/users/{user_id}")
  2490. async def delete_user(
  2491. user_id: str,
  2492. credentials: HTTPAuthorizationCredentials = Depends(security)
  2493. ):
  2494. """删除用户"""
  2495. try:
  2496. payload = verify_token(credentials.credentials)
  2497. if not payload:
  2498. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2499. is_superuser = payload.get("is_superuser", False)
  2500. if not is_superuser:
  2501. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2502. # 不能删除自己
  2503. if user_id == payload.get("sub"):
  2504. return ApiResponse(code=400, message="不能删除自己", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2505. conn = get_db_connection()
  2506. if not conn:
  2507. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2508. cursor = conn.cursor()
  2509. # 检查是否为超级管理员
  2510. cursor.execute("""
  2511. SELECT COUNT(*) FROM user_roles ur
  2512. JOIN roles r ON ur.role_id = r.id
  2513. WHERE ur.user_id = %s AND r.name = 'super_admin' AND ur.is_active = 1
  2514. """, (user_id,))
  2515. if cursor.fetchone()[0] > 0:
  2516. cursor.close()
  2517. conn.close()
  2518. return ApiResponse(code=400, message="不能删除超级管理员", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2519. # 删除相关数据
  2520. cursor.execute("DELETE FROM user_roles WHERE user_id = %s", (user_id,))
  2521. cursor.execute("DELETE FROM user_profiles WHERE user_id = %s", (user_id,))
  2522. cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
  2523. conn.commit()
  2524. cursor.close()
  2525. conn.close()
  2526. return ApiResponse(code=0, message="用户删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2527. except Exception as e:
  2528. print(f"删除用户错误: {e}")
  2529. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2530. # 角色管理API
  2531. @app.post("/api/v1/admin/roles")
  2532. async def create_role(
  2533. role_data: dict,
  2534. credentials: HTTPAuthorizationCredentials = Depends(security)
  2535. ):
  2536. """创建角色"""
  2537. try:
  2538. payload = verify_token(credentials.credentials)
  2539. if not payload:
  2540. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2541. is_superuser = payload.get("is_superuser", False)
  2542. if not is_superuser:
  2543. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2544. conn = get_db_connection()
  2545. if not conn:
  2546. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2547. cursor = conn.cursor()
  2548. # 检查角色名是否已存在
  2549. cursor.execute("SELECT id FROM roles WHERE name = %s", (role_data['name'],))
  2550. if cursor.fetchone():
  2551. cursor.close()
  2552. conn.close()
  2553. return ApiResponse(code=400, message="角色名已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2554. # 创建角色
  2555. role_id = str(uuid.uuid4())
  2556. cursor.execute("""
  2557. INSERT INTO roles (id, name, display_name, description, is_active, is_system, created_at, updated_at)
  2558. VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
  2559. """, (role_id, role_data['name'], role_data['display_name'], role_data.get('description'),
  2560. role_data.get('is_active', True), False))
  2561. conn.commit()
  2562. cursor.close()
  2563. conn.close()
  2564. return ApiResponse(code=0, message="角色创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2565. except Exception as e:
  2566. print(f"创建角色错误: {e}")
  2567. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2568. @app.put("/api/v1/admin/roles/{role_id}")
  2569. async def update_role(
  2570. role_id: str,
  2571. role_data: dict,
  2572. credentials: HTTPAuthorizationCredentials = Depends(security)
  2573. ):
  2574. """更新角色"""
  2575. try:
  2576. payload = verify_token(credentials.credentials)
  2577. if not payload:
  2578. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2579. is_superuser = payload.get("is_superuser", False)
  2580. if not is_superuser:
  2581. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2582. conn = get_db_connection()
  2583. if not conn:
  2584. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2585. cursor = conn.cursor()
  2586. # 检查是否为系统角色
  2587. cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
  2588. role = cursor.fetchone()
  2589. if not role:
  2590. cursor.close()
  2591. conn.close()
  2592. return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2593. if role[0]: # is_system
  2594. cursor.close()
  2595. conn.close()
  2596. return ApiResponse(code=400, message="不能修改系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2597. # 更新角色
  2598. update_fields = []
  2599. update_values = []
  2600. for field in ['display_name', 'description', 'is_active']:
  2601. if field in role_data:
  2602. update_fields.append(f'{field} = %s')
  2603. update_values.append(role_data[field])
  2604. if update_fields:
  2605. update_values.append(role_id)
  2606. cursor.execute(f"""
  2607. UPDATE roles
  2608. SET {', '.join(update_fields)}, updated_at = NOW()
  2609. WHERE id = %s
  2610. """, update_values)
  2611. conn.commit()
  2612. cursor.close()
  2613. conn.close()
  2614. return ApiResponse(code=0, message="角色更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2615. except Exception as e:
  2616. print(f"更新角色错误: {e}")
  2617. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2618. @app.delete("/api/v1/admin/roles/{role_id}")
  2619. async def delete_role(
  2620. role_id: str,
  2621. credentials: HTTPAuthorizationCredentials = Depends(security)
  2622. ):
  2623. """删除角色"""
  2624. try:
  2625. payload = verify_token(credentials.credentials)
  2626. if not payload:
  2627. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2628. is_superuser = payload.get("is_superuser", False)
  2629. if not is_superuser:
  2630. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2631. conn = get_db_connection()
  2632. if not conn:
  2633. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2634. cursor = conn.cursor()
  2635. # 检查是否为系统角色
  2636. cursor.execute("SELECT is_system FROM roles WHERE id = %s", (role_id,))
  2637. role = cursor.fetchone()
  2638. if not role:
  2639. cursor.close()
  2640. conn.close()
  2641. return ApiResponse(code=404, message="角色不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2642. if role[0]: # is_system
  2643. cursor.close()
  2644. conn.close()
  2645. return ApiResponse(code=400, message="不能删除系统角色", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2646. # 检查是否有用户使用此角色
  2647. cursor.execute("SELECT COUNT(*) FROM user_roles WHERE role_id = %s", (role_id,))
  2648. if cursor.fetchone()[0] > 0:
  2649. cursor.close()
  2650. conn.close()
  2651. return ApiResponse(code=400, message="该角色正在被使用,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2652. # 删除角色相关数据
  2653. cursor.execute("DELETE FROM role_permissions WHERE role_id = %s", (role_id,))
  2654. cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
  2655. cursor.execute("DELETE FROM roles WHERE id = %s", (role_id,))
  2656. conn.commit()
  2657. cursor.close()
  2658. conn.close()
  2659. return ApiResponse(code=0, message="角色删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2660. except Exception as e:
  2661. print(f"删除角色错误: {e}")
  2662. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2663. # 角色菜单权限管理API
  2664. @app.get("/api/v1/admin/roles/{role_id}/menus")
  2665. async def get_role_menus(
  2666. role_id: str,
  2667. credentials: HTTPAuthorizationCredentials = Depends(security)
  2668. ):
  2669. """获取角色的菜单权限"""
  2670. try:
  2671. payload = verify_token(credentials.credentials)
  2672. if not payload:
  2673. return ApiResponse(
  2674. code=401,
  2675. message="无效的访问令牌",
  2676. timestamp=datetime.now(timezone.utc).isoformat()
  2677. ).model_dump()
  2678. # 检查管理员权限
  2679. is_superuser = payload.get("is_superuser", False)
  2680. if not is_superuser:
  2681. return ApiResponse(
  2682. code=403,
  2683. message="权限不足",
  2684. timestamp=datetime.now(timezone.utc).isoformat()
  2685. ).model_dump()
  2686. conn = get_db_connection()
  2687. if not conn:
  2688. return ApiResponse(
  2689. code=500,
  2690. message="数据库连接失败",
  2691. timestamp=datetime.now(timezone.utc).isoformat()
  2692. ).model_dump()
  2693. cursor = conn.cursor()
  2694. # 检查角色是否存在
  2695. cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
  2696. role = cursor.fetchone()
  2697. if not role:
  2698. cursor.close()
  2699. conn.close()
  2700. return ApiResponse(
  2701. code=404,
  2702. message="角色不存在",
  2703. timestamp=datetime.now(timezone.utc).isoformat()
  2704. ).model_dump()
  2705. # 检查是否为超级管理员角色
  2706. role_name = role[1]
  2707. is_super_admin_role = role_name == "super_admin"
  2708. if is_super_admin_role:
  2709. # 超级管理员默认拥有所有菜单权限
  2710. cursor.execute("""
  2711. SELECT id, name, title, parent_id, menu_type
  2712. FROM menus
  2713. WHERE is_active = 1
  2714. ORDER BY sort_order
  2715. """)
  2716. menu_permissions = cursor.fetchall()
  2717. else:
  2718. # 普通角色查询已分配的菜单权限
  2719. cursor.execute("""
  2720. SELECT m.id, m.name, m.title, m.parent_id, m.menu_type
  2721. FROM role_menus rm
  2722. JOIN menus m ON rm.menu_id = m.id
  2723. WHERE rm.role_id = %s AND m.is_active = 1
  2724. ORDER BY m.sort_order
  2725. """, (role_id,))
  2726. menu_permissions = cursor.fetchall()
  2727. cursor.close()
  2728. conn.close()
  2729. # 构建返回数据
  2730. menu_ids = [menu[0] for menu in menu_permissions]
  2731. menu_details = []
  2732. for menu in menu_permissions:
  2733. menu_details.append({
  2734. "id": menu[0],
  2735. "name": menu[1],
  2736. "title": menu[2],
  2737. "parent_id": menu[3],
  2738. "menu_type": menu[4]
  2739. })
  2740. return ApiResponse(
  2741. code=0,
  2742. message="获取角色菜单权限成功",
  2743. data={
  2744. "role_id": role_id,
  2745. "role_name": role[1],
  2746. "menu_ids": menu_ids,
  2747. "menu_details": menu_details,
  2748. "total": len(menu_ids)
  2749. },
  2750. timestamp=datetime.now(timezone.utc).isoformat()
  2751. ).model_dump()
  2752. except Exception as e:
  2753. print(f"获取角色菜单权限错误: {e}")
  2754. return ApiResponse(
  2755. code=500,
  2756. message="服务器内部错误",
  2757. timestamp=datetime.now(timezone.utc).isoformat()
  2758. ).model_dump()
  2759. @app.put("/api/v1/admin/roles/{role_id}/menus")
  2760. async def update_role_menus(
  2761. role_id: str,
  2762. request: Request,
  2763. credentials: HTTPAuthorizationCredentials = Depends(security)
  2764. ):
  2765. """更新角色的菜单权限"""
  2766. try:
  2767. payload = verify_token(credentials.credentials)
  2768. if not payload:
  2769. return ApiResponse(
  2770. code=401,
  2771. message="无效的访问令牌",
  2772. timestamp=datetime.now(timezone.utc).isoformat()
  2773. ).model_dump()
  2774. # 检查管理员权限
  2775. is_superuser = payload.get("is_superuser", False)
  2776. if not is_superuser:
  2777. return ApiResponse(
  2778. code=403,
  2779. message="权限不足",
  2780. timestamp=datetime.now(timezone.utc).isoformat()
  2781. ).model_dump()
  2782. # 获取请求数据
  2783. body = await request.json()
  2784. menu_ids = body.get("menu_ids", [])
  2785. if not isinstance(menu_ids, list):
  2786. return ApiResponse(
  2787. code=400,
  2788. message="菜单ID列表格式错误",
  2789. timestamp=datetime.now(timezone.utc).isoformat()
  2790. ).model_dump()
  2791. conn = get_db_connection()
  2792. if not conn:
  2793. return ApiResponse(
  2794. code=500,
  2795. message="数据库连接失败",
  2796. timestamp=datetime.now(timezone.utc).isoformat()
  2797. ).model_dump()
  2798. cursor = conn.cursor()
  2799. # 检查角色是否存在
  2800. cursor.execute("SELECT id, name FROM roles WHERE id = %s", (role_id,))
  2801. role = cursor.fetchone()
  2802. if not role:
  2803. cursor.close()
  2804. conn.close()
  2805. return ApiResponse(
  2806. code=404,
  2807. message="角色不存在",
  2808. timestamp=datetime.now(timezone.utc).isoformat()
  2809. ).model_dump()
  2810. # 检查是否为超级管理员角色
  2811. role_name = role[1]
  2812. is_super_admin_role = role_name == "super_admin"
  2813. if is_super_admin_role:
  2814. # 超级管理员角色不允许修改权限,始终拥有全部权限
  2815. cursor.close()
  2816. conn.close()
  2817. return ApiResponse(
  2818. code=400,
  2819. message="超级管理员角色拥有全部权限,无需修改",
  2820. timestamp=datetime.now(timezone.utc).isoformat()
  2821. ).model_dump()
  2822. # 验证菜单ID是否存在
  2823. if menu_ids:
  2824. placeholders = ','.join(['%s'] * len(menu_ids))
  2825. cursor.execute(f"""
  2826. SELECT id FROM menus
  2827. WHERE id IN ({placeholders}) AND is_active = 1
  2828. """, menu_ids)
  2829. valid_menu_ids = [row[0] for row in cursor.fetchall()]
  2830. invalid_menu_ids = set(menu_ids) - set(valid_menu_ids)
  2831. if invalid_menu_ids:
  2832. cursor.close()
  2833. conn.close()
  2834. return ApiResponse(
  2835. code=400,
  2836. message=f"无效的菜单ID: {', '.join(invalid_menu_ids)}",
  2837. timestamp=datetime.now(timezone.utc).isoformat()
  2838. ).model_dump()
  2839. # 开始事务
  2840. cursor.execute("START TRANSACTION")
  2841. try:
  2842. # 删除角色现有的菜单权限
  2843. cursor.execute("DELETE FROM role_menus WHERE role_id = %s", (role_id,))
  2844. # 添加新的菜单权限
  2845. if menu_ids:
  2846. values = [(role_id, menu_id) for menu_id in menu_ids]
  2847. cursor.executemany("""
  2848. INSERT INTO role_menus (role_id, menu_id, created_at)
  2849. VALUES (%s, %s, NOW())
  2850. """, values)
  2851. # 提交事务
  2852. conn.commit()
  2853. cursor.close()
  2854. conn.close()
  2855. return ApiResponse(
  2856. code=0,
  2857. message="角色菜单权限更新成功",
  2858. data={
  2859. "role_id": role_id,
  2860. "role_name": role[1],
  2861. "menu_ids": menu_ids,
  2862. "updated_count": len(menu_ids)
  2863. },
  2864. timestamp=datetime.now(timezone.utc).isoformat()
  2865. ).model_dump()
  2866. except Exception as e:
  2867. # 回滚事务
  2868. conn.rollback()
  2869. cursor.close()
  2870. conn.close()
  2871. raise e
  2872. except Exception as e:
  2873. print(f"更新角色菜单权限错误: {e}")
  2874. return ApiResponse(
  2875. code=500,
  2876. message="服务器内部错误",
  2877. timestamp=datetime.now(timezone.utc).isoformat()
  2878. ).model_dump()
  2879. # 菜单管理API
  2880. @app.post("/api/v1/admin/menus")
  2881. async def create_menu(
  2882. menu_data: dict,
  2883. credentials: HTTPAuthorizationCredentials = Depends(security)
  2884. ):
  2885. """创建菜单"""
  2886. try:
  2887. payload = verify_token(credentials.credentials)
  2888. if not payload:
  2889. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2890. is_superuser = payload.get("is_superuser", False)
  2891. if not is_superuser:
  2892. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2893. conn = get_db_connection()
  2894. if not conn:
  2895. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2896. cursor = conn.cursor()
  2897. # 检查菜单名是否已存在
  2898. cursor.execute("SELECT id FROM menus WHERE name = %s", (menu_data['name'],))
  2899. if cursor.fetchone():
  2900. cursor.close()
  2901. conn.close()
  2902. return ApiResponse(code=400, message="菜单标识已存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2903. # 创建菜单
  2904. menu_id = str(uuid.uuid4())
  2905. cursor.execute("""
  2906. INSERT INTO menus (id, parent_id, name, title, path, component, icon,
  2907. sort_order, menu_type, is_hidden, is_active, description, created_at, updated_at)
  2908. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
  2909. """, (
  2910. menu_id, menu_data.get('parent_id'), menu_data['name'], menu_data['title'],
  2911. menu_data.get('path'), menu_data.get('component'), menu_data.get('icon'),
  2912. menu_data.get('sort_order', 0), menu_data.get('menu_type', 'menu'),
  2913. menu_data.get('is_hidden', False), menu_data.get('is_active', True),
  2914. menu_data.get('description')
  2915. ))
  2916. conn.commit()
  2917. cursor.close()
  2918. conn.close()
  2919. return ApiResponse(code=0, message="菜单创建成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2920. except Exception as e:
  2921. print(f"创建菜单错误: {e}")
  2922. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2923. @app.put("/api/v1/admin/menus/{menu_id}")
  2924. async def update_menu(
  2925. menu_id: str,
  2926. menu_data: dict,
  2927. credentials: HTTPAuthorizationCredentials = Depends(security)
  2928. ):
  2929. """更新菜单"""
  2930. try:
  2931. payload = verify_token(credentials.credentials)
  2932. if not payload:
  2933. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2934. is_superuser = payload.get("is_superuser", False)
  2935. if not is_superuser:
  2936. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2937. conn = get_db_connection()
  2938. if not conn:
  2939. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2940. cursor = conn.cursor()
  2941. # 更新菜单
  2942. update_fields = []
  2943. update_values = []
  2944. for field in ['parent_id', 'title', 'path', 'component', 'icon', 'sort_order',
  2945. 'menu_type', 'is_hidden', 'is_active', 'description']:
  2946. if field in menu_data:
  2947. update_fields.append(f'{field} = %s')
  2948. update_values.append(menu_data[field])
  2949. if update_fields:
  2950. update_values.append(menu_id)
  2951. cursor.execute(f"""
  2952. UPDATE menus
  2953. SET {', '.join(update_fields)}, updated_at = NOW()
  2954. WHERE id = %s
  2955. """, update_values)
  2956. conn.commit()
  2957. cursor.close()
  2958. conn.close()
  2959. return ApiResponse(code=0, message="菜单更新成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2960. except Exception as e:
  2961. print(f"更新菜单错误: {e}")
  2962. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2963. @app.delete("/api/v1/admin/menus/{menu_id}")
  2964. async def delete_menu(
  2965. menu_id: str,
  2966. credentials: HTTPAuthorizationCredentials = Depends(security)
  2967. ):
  2968. """删除菜单"""
  2969. try:
  2970. payload = verify_token(credentials.credentials)
  2971. if not payload:
  2972. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2973. is_superuser = payload.get("is_superuser", False)
  2974. if not is_superuser:
  2975. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2976. conn = get_db_connection()
  2977. if not conn:
  2978. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2979. cursor = conn.cursor()
  2980. # 检查是否有子菜单
  2981. cursor.execute("SELECT COUNT(*) FROM menus WHERE parent_id = %s", (menu_id,))
  2982. if cursor.fetchone()[0] > 0:
  2983. cursor.close()
  2984. conn.close()
  2985. return ApiResponse(code=400, message="该菜单下有子菜单,无法删除", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2986. # 删除菜单相关数据
  2987. cursor.execute("DELETE FROM role_menus WHERE menu_id = %s", (menu_id,))
  2988. cursor.execute("DELETE FROM menus WHERE id = %s", (menu_id,))
  2989. conn.commit()
  2990. cursor.close()
  2991. conn.close()
  2992. return ApiResponse(code=0, message="菜单删除成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2993. except Exception as e:
  2994. print(f"删除菜单错误: {e}")
  2995. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  2996. # 获取所有角色(用于下拉选择)
  2997. @app.get("/api/v1/roles/all")
  2998. async def get_all_roles_simple(credentials: HTTPAuthorizationCredentials = Depends(security)):
  2999. """获取所有角色(简化版,用于下拉选择)"""
  3000. try:
  3001. payload = verify_token(credentials.credentials)
  3002. if not payload:
  3003. return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3004. conn = get_db_connection()
  3005. if not conn:
  3006. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3007. cursor = conn.cursor()
  3008. cursor.execute("""
  3009. SELECT id, name, display_name, is_system, is_active
  3010. FROM roles
  3011. WHERE is_active = 1
  3012. ORDER BY is_system DESC, display_name
  3013. """)
  3014. roles = []
  3015. for row in cursor.fetchall():
  3016. roles.append({
  3017. "id": row[0],
  3018. "name": row[1],
  3019. "display_name": row[2],
  3020. "is_system": bool(row[3]),
  3021. "is_active": bool(row[4])
  3022. })
  3023. cursor.close()
  3024. conn.close()
  3025. return ApiResponse(code=0, message="获取角色列表成功", data=roles, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3026. except Exception as e:
  3027. print(f"获取角色列表错误: {e}")
  3028. return ApiResponse(code=500, message="服务器内部错误", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3029. import httpx
  3030. from fastapi.responses import HTMLResponse
  3031. class BatchEnterRequest(BaseModel):
  3032. ids: list[int]
  3033. table_type: Optional[str] = "basis"
  3034. class BatchDeleteRequest(BaseModel):
  3035. ids: list[Union[int, str]]
  3036. table_type: Optional[str] = "basis"
  3037. class ConvertRequest(BaseModel):
  3038. id: Union[int, str]
  3039. table_type: Optional[str] = "basis"
  3040. # --- 文档管理中心 API ---
  3041. @app.get("/api/v1/documents/proxy-view")
  3042. async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)):
  3043. """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。"""
  3044. try:
  3045. # 优先从 Header 获取,如果没有则从参数获取
  3046. actual_token = None
  3047. if credentials:
  3048. actual_token = credentials.credentials
  3049. elif token:
  3050. actual_token = token
  3051. if not actual_token:
  3052. return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3053. payload = verify_token(actual_token)
  3054. if not payload or not payload.get("is_superuser"):
  3055. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3056. # 增加超时时间,支持大文件下载
  3057. async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
  3058. headers = {
  3059. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  3060. }
  3061. response = await client.get(url, headers=headers)
  3062. response.raise_for_status()
  3063. content_type = response.headers.get("content-type", "").lower()
  3064. # 如果是 PDF 或其他二进制文件
  3065. if "application/pdf" in content_type or any(ext in url.lower() for ext in [".pdf", ".png", ".jpg", ".jpeg", ".gif"]):
  3066. return Response(
  3067. content=response.content,
  3068. media_type=content_type,
  3069. headers={"Content-Disposition": "inline"}
  3070. )
  3071. # 默认处理为 HTML
  3072. try:
  3073. content = response.text
  3074. # 简单的注入一些基础样式,确保内容在 iframe 中显示良好
  3075. base_style = """
  3076. <style>
  3077. body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 20px; line-height: 1.6; color: #333; }
  3078. img { max-width: 100%; height: auto; }
  3079. </style>
  3080. """
  3081. if "</head>" in content:
  3082. content = content.replace("</head>", f"{base_style}</head>")
  3083. else:
  3084. content = f"{base_style}{content}"
  3085. return HTMLResponse(content=content)
  3086. except Exception:
  3087. # 如果文本解析失败,返回原始字节
  3088. return Response(content=response.content, media_type=content_type)
  3089. except Exception as e:
  3090. error_msg = f"<html><body><h3>无法加载内容</h3><p>错误原因: {str(e)}</p><p>URL: {url}</p></body></html>"
  3091. return HTMLResponse(content=error_msg, status_code=500)
  3092. @app.post("/api/v1/documents/batch-enter")
  3093. async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  3094. """批量将文档加入知识库"""
  3095. try:
  3096. payload = verify_token(credentials.credentials)
  3097. if not payload or not payload.get("is_superuser"):
  3098. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3099. conn = get_db_connection()
  3100. if not conn:
  3101. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3102. cursor = conn.cursor()
  3103. table_name = get_table_name(req.table_type)
  3104. # 批量更新 whether_to_enter 为 1
  3105. # 只更新尚未入库的数据 (whether_to_enter = 0)
  3106. placeholders = ', '.join(['%s'] * len(req.ids))
  3107. sql = f"UPDATE {table_name} SET whether_to_enter = 1, updated_at = NOW() WHERE id IN ({placeholders}) AND whether_to_enter = 0"
  3108. cursor.execute(sql, req.ids)
  3109. conn.commit()
  3110. affected_rows = cursor.rowcount
  3111. cursor.close()
  3112. conn.close()
  3113. message = f"成功将 {affected_rows} 条数据加入知识库"
  3114. if affected_rows < len(req.ids):
  3115. message += f"(跳过了 {len(req.ids) - affected_rows} 条已入库数据)"
  3116. return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3117. except Exception as e:
  3118. return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3119. @app.post("/api/v1/documents/batch-delete")
  3120. async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  3121. """批量删除文档"""
  3122. conn = None
  3123. cursor = None
  3124. try:
  3125. payload = verify_token(credentials.credentials)
  3126. if not payload or not payload.get("is_superuser"):
  3127. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3128. conn = get_db_connection()
  3129. if not conn:
  3130. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3131. cursor = conn.cursor()
  3132. table_name = get_table_name(req.table_type)
  3133. if not req.ids:
  3134. return ApiResponse(code=400, message="未指定要删除的文档 ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3135. placeholders = ', '.join(['%s'] * len(req.ids))
  3136. sql = f"DELETE FROM {table_name} WHERE id IN ({placeholders})"
  3137. cursor.execute(sql, req.ids)
  3138. conn.commit()
  3139. affected_rows = cursor.rowcount
  3140. return ApiResponse(
  3141. code=0,
  3142. message=f"成功删除 {affected_rows} 条文档数据",
  3143. timestamp=datetime.now(timezone.utc).isoformat()
  3144. ).model_dump()
  3145. except Exception as e:
  3146. print(f"批量删除失败: {e}")
  3147. return ApiResponse(code=500, message=f"批量删除失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3148. finally:
  3149. if cursor:
  3150. cursor.close()
  3151. if conn:
  3152. conn.close()
  3153. @app.post("/api/v1/documents/convert")
  3154. async def convert_document(req: ConvertRequest, credentials: HTTPAuthorizationCredentials = Depends(security)):
  3155. """异步启动文档转换"""
  3156. import subprocess
  3157. try:
  3158. payload = verify_token(credentials.credentials)
  3159. if not payload or not payload.get("is_superuser"):
  3160. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3161. # 启动后台进程执行转换
  3162. # 脚本位于 d:\UGit\LQAdminPlatform\scripts\miner_u.py
  3163. script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "scripts", "miner_u.py"))
  3164. # 使用当前 python 解释器
  3165. python_exe = sys.executable
  3166. # 异步启动,不等待结束
  3167. subprocess.Popen([python_exe, script_path, str(req.table_type), str(req.id)],
  3168. stdout=subprocess.DEVNULL,
  3169. stderr=subprocess.DEVNULL,
  3170. creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
  3171. return ApiResponse(
  3172. code=0,
  3173. message="转换任务已启动",
  3174. timestamp=datetime.now(timezone.utc).isoformat()
  3175. ).model_dump()
  3176. except Exception as e:
  3177. print(f"启动转换失败: {e}")
  3178. return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3179. @app.post("/api/v1/documents/add")
  3180. async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)):
  3181. """添加新文档"""
  3182. try:
  3183. payload = verify_token(credentials.credentials)
  3184. if not payload or not payload.get("is_superuser"):
  3185. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3186. conn = get_db_connection()
  3187. if not conn:
  3188. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3189. cursor = conn.cursor()
  3190. table_name = get_table_name(doc.table_type)
  3191. # 修正列名:reference_basis -> reference_basis_list
  3192. sql = f"""
  3193. INSERT INTO {table_name}
  3194. (chinese_name, reference_basis_list, document_type, professional_field, release_date, created_at, updated_at)
  3195. VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
  3196. """
  3197. # 构造日期:如果是年份,转为 YYYY-01-01
  3198. release_date = f"{doc.year}-01-01" if doc.year else None
  3199. cursor.execute(sql, (doc.title, doc.content, str(doc.primary_category_id) if doc.primary_category_id else None,
  3200. str(doc.secondary_category_id) if doc.secondary_category_id else None, release_date))
  3201. conn.commit()
  3202. cursor.close()
  3203. conn.close()
  3204. return ApiResponse(code=0, message="文档添加成功", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3205. except Exception as e:
  3206. print(f"添加文档错误: {e}")
  3207. return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3208. @app.get("/api/v1/documents/list")
  3209. async def get_document_list(
  3210. primaryCategoryId: Optional[str] = None,
  3211. secondaryCategoryId: Optional[str] = None,
  3212. year: Optional[int] = None,
  3213. whether_to_enter: Optional[int] = None,
  3214. keyword: Optional[str] = None,
  3215. table_type: Optional[str] = "basis",
  3216. page: int = 1,
  3217. size: int = 50,
  3218. sort_by: str = "created_at",
  3219. credentials: HTTPAuthorizationCredentials = Depends(security)
  3220. ):
  3221. """获取文档列表(支持过滤与搜索)"""
  3222. conn = None
  3223. cursor = None
  3224. try:
  3225. payload = verify_token(credentials.credentials)
  3226. if not payload or not payload.get("is_superuser"):
  3227. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3228. conn = get_db_connection()
  3229. if not conn:
  3230. return ApiResponse(code=500, message="数据库连接失败", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3231. cursor = conn.cursor()
  3232. table_name = get_table_name(table_type)
  3233. where_clauses = []
  3234. params = []
  3235. if primaryCategoryId:
  3236. where_clauses.append("document_type = %s")
  3237. params.append(primaryCategoryId)
  3238. if secondaryCategoryId:
  3239. where_clauses.append("professional_field = %s")
  3240. params.append(secondaryCategoryId)
  3241. if year:
  3242. where_clauses.append("YEAR(release_date) = %s")
  3243. params.append(year)
  3244. if whether_to_enter is not None:
  3245. where_clauses.append("CAST(whether_to_enter AS UNSIGNED) = %s")
  3246. params.append(whether_to_enter)
  3247. if keyword:
  3248. where_clauses.append("(chinese_name LIKE %s OR reference_basis_list LIKE %s OR standard_no LIKE %s)")
  3249. like_keyword = f"%{keyword}%"
  3250. params.extend([like_keyword, like_keyword, like_keyword])
  3251. where_stmt = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
  3252. # 排序逻辑:按创建时间倒序
  3253. sort_field = "created_at" if sort_by == "created_at" else "updated_at"
  3254. order_by = f"ORDER BY {sort_field} DESC"
  3255. # 分页
  3256. offset = (page - 1) * size
  3257. # 返回更多字段
  3258. sql = f"""
  3259. SELECT id, chinese_name as title, reference_basis_list as content,
  3260. document_type, professional_field,
  3261. YEAR(release_date) as year, release_date, standard_no, status,
  3262. CAST(whether_to_enter AS UNSIGNED) as whether_to_enter, file_url,
  3263. conversion_status, conversion_progress, conversion_error,
  3264. created_at, updated_at
  3265. FROM {table_name} {where_stmt}
  3266. {order_by} LIMIT %s OFFSET %s
  3267. """
  3268. params.extend([size, offset])
  3269. cursor.execute(sql, params)
  3270. columns = [col[0] for col in cursor.description]
  3271. items = [dict(zip(columns, row)) for row in cursor.fetchall()]
  3272. # 格式化时间
  3273. for item in items:
  3274. for key, value in item.items():
  3275. if isinstance(value, (datetime, date)):
  3276. item[key] = value.isoformat()
  3277. # 获取总数
  3278. count_sql = f"SELECT COUNT(*) FROM {table_name} {where_stmt}"
  3279. cursor.execute(count_sql, params[:-2])
  3280. total = cursor.fetchone()[0]
  3281. # 优化统计查询:合并全局总数和已入库总数的查询,减少数据库交互
  3282. stats_sql = f"SELECT COUNT(*), SUM(CASE WHEN CAST(whether_to_enter AS UNSIGNED) = 1 THEN 1 ELSE 0 END) FROM {table_name}"
  3283. cursor.execute(stats_sql)
  3284. stats_result = cursor.fetchone()
  3285. all_total = 0
  3286. total_entered = 0
  3287. if stats_result:
  3288. all_total = stats_result[0] or 0
  3289. total_entered = int(stats_result[1] or 0)
  3290. return ApiResponse(
  3291. code=0,
  3292. message="获取成功",
  3293. data={
  3294. "items": items,
  3295. "total": total,
  3296. "all_total": all_total,
  3297. "total_entered": total_entered,
  3298. "page": page,
  3299. "size": size
  3300. },
  3301. timestamp=datetime.now(timezone.utc).isoformat()
  3302. ).model_dump()
  3303. except Exception as e:
  3304. print(f"获取文档列表错误: {e}")
  3305. return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3306. finally:
  3307. if cursor:
  3308. cursor.close()
  3309. if conn:
  3310. conn.close()
  3311. @app.get("/api/v1/documents/categories/primary")
  3312. async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)):
  3313. """获取所有一级分类(仅保留指定的分类)"""
  3314. try:
  3315. payload = verify_token(credentials.credentials)
  3316. if not payload or not payload.get("is_superuser"):
  3317. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3318. # 仅保留用户要求的分类
  3319. default_categories = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"]
  3320. categories = [{"id": name, "name": name} for name in default_categories]
  3321. return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3322. except Exception as e:
  3323. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3324. @app.get("/api/v1/documents/categories/secondary")
  3325. async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)):
  3326. """根据一级分类获取二级分类(仅保留指定的分类)"""
  3327. try:
  3328. payload = verify_token(credentials.credentials)
  3329. if not payload or not payload.get("is_superuser"):
  3330. return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3331. # 针对“办公制度”的预设二级分类,其他分类暂时没有二级分类
  3332. categories = []
  3333. if primaryId == "办公制度":
  3334. secondary_names = ["采购", "报销", "审批"]
  3335. categories = [{"id": name, "name": name} for name in secondary_names]
  3336. return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3337. except Exception as e:
  3338. return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump()
  3339. @app.get("/api/v1/documents/search")
  3340. async def search_documents(
  3341. keyword: str,
  3342. primaryCategoryId: Optional[str] = None,
  3343. secondaryCategoryId: Optional[str] = None,
  3344. year: Optional[int] = None,
  3345. whether_to_enter: Optional[int] = None,
  3346. table_type: Optional[str] = "basis",
  3347. page: int = 1,
  3348. size: int = 50,
  3349. credentials: HTTPAuthorizationCredentials = Depends(security)
  3350. ):
  3351. """关键词搜索文档,统一调用 get_document_list 以支持组合过滤"""
  3352. return await get_document_list(
  3353. primaryCategoryId=primaryCategoryId,
  3354. secondaryCategoryId=secondaryCategoryId,
  3355. year=year,
  3356. whether_to_enter=whether_to_enter,
  3357. keyword=keyword,
  3358. table_type=table_type,
  3359. page=page,
  3360. size=size,
  3361. credentials=credentials
  3362. )
  3363. if __name__ == "__main__":
  3364. import uvicorn
  3365. # 查找可用端口
  3366. port = find_available_port()
  3367. if port is None:
  3368. print("❌ 无法找到可用端口 (8000-8010)")
  3369. print("请手动停止占用这些端口的进程")
  3370. sys.exit(1)
  3371. print("=" * 60)
  3372. print("🚀 SSO认证中心完整服务器")
  3373. print("=" * 60)
  3374. print(f"✅ 找到可用端口: {port}")
  3375. print(f"🌐 访问地址: http://localhost:{port}")
  3376. print(f"📚 API文档: http://localhost:{port}/docs")
  3377. print(f"❤️ 健康检查: http://localhost:{port}/health")
  3378. print(f"🔐 登录API: http://localhost:{port}/api/v1/auth/login")
  3379. print("=" * 60)
  3380. print("📝 前端配置:")
  3381. print(f" VITE_API_BASE_URL=http://localhost:{port}")
  3382. print("=" * 60)
  3383. print("👤 测试账号:")
  3384. print(" 用户名: admin")
  3385. print(" 密码: Admin123456")
  3386. print("=" * 60)
  3387. print("按 Ctrl+C 停止服务器")
  3388. print()
  3389. try:
  3390. uvicorn.run(
  3391. app,
  3392. host="0.0.0.0",
  3393. port=port,
  3394. log_level="info"
  3395. )
  3396. except KeyboardInterrupt:
  3397. print("\n👋 服务器已停止")
  3398. except Exception as e:
  3399. print(f"❌ 启动失败: {e}")
  3400. sys.exit(1)
  3401. @app.get("/api/v1/auth/captcha")
  3402. async def get_captcha():
  3403. """获取验证码"""
  3404. try:
  3405. # 生成验证码
  3406. captcha_text, captcha_image = generate_captcha()
  3407. # 这里应该将验证码文本存储到缓存中(Redis或内存)
  3408. # 为了简化,我们暂时返回固定的验证码
  3409. captcha_id = secrets.token_hex(16)
  3410. return ApiResponse(
  3411. code=0,
  3412. message="获取验证码成功",
  3413. data={
  3414. "captcha_id": captcha_id,
  3415. "captcha_image": captcha_image,
  3416. "captcha_text": captcha_text # 生产环境中不应该返回这个
  3417. },
  3418. timestamp=datetime.now(timezone.utc).isoformat()
  3419. ).model_dump()
  3420. except Exception as e:
  3421. print(f"生成验证码错误: {e}")
  3422. return ApiResponse(
  3423. code=500001,
  3424. message="生成验证码失败",
  3425. timestamp=datetime.now(timezone.utc).isoformat()
  3426. ).model_dump()
  3427. def generate_captcha():
  3428. """生成验证码"""
  3429. try:
  3430. import random
  3431. import string
  3432. import base64
  3433. # 生成随机验证码文本
  3434. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  3435. # 创建一个简单的SVG验证码
  3436. svg_captcha = f"""
  3437. <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
  3438. <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
  3439. <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
  3440. </svg>
  3441. """
  3442. svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
  3443. return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
  3444. except Exception as e:
  3445. print(f"生成验证码失败: {e}")
  3446. # 返回默认验证码
  3447. return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="