full_server.py 171 KB

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