
關鍵詞FastAPI · Tortoise ORM · Redis · JWT · Vue3 · 企業級登錄 · 驗證碼 · Elasticsearch一、背景項目是一個 BOSS 直聘類招聘平臺分三端企業端boss-company-ui企業招聘、職位發布、團隊管理管理端boss-manage-ui企業管理、認證審核、職位管理求職者端boss-candidate-ui后端是 FastAPI Tortoise ORM MySQL Redis登錄采用JWTjoseHS256驗證碼存 Rediskeyboss-api:enterprise-login:sms:{mobile}。企業端登錄入口POST /enterprise/login要同時兼容兩類賬號招聘團隊成員RecruitTeam手機號 驗證碼企業負責人EnterpriseQualification 資質聯系人且企業已審核通過今天在聯調過程中登錄鏈路連續踩了 4 個坑下面按出現順序復盤。二、坑 1登錄成功后又被彈回登錄頁現象頁面提示“登錄成功”但瞬間又跳回登錄頁。根因/enterprise/login路由只調用了team_login要求手機號在RecruitTeam且statusNORMAL而企業負責人專用的EnterpriseService.login方法存在卻沒有掛載到任何路由。結果已注冊/認證、但未加入 RecruitTeam 的企業負責人根本無法登錄前端拿不到 token自然停在登錄頁。修復把企業主登錄兜底掛上并補全返回字段# app/apis/enterprise_api.pyenterprise_router.post(/login)asyncdeflogin(loginMobileRequest:LoginMobileRequest):resawaitEnterpriseService.login(loginMobileRequest)return{code:1,message:登錄成功,data:res}# app/services/enterprise_service.pyasyncdeflogin(loginMobileRequest):mobile,codeloginMobileRequest.mobile,loginMobileRequest.code EnterpriseService._verify_sms_code(mobile,code)# 1) 優先團隊成員recruit_teamawaitRecruitTeam.get_or_none(mobilemobile,statusTeamMemberStatus.NORMAL,is_deletedDeleteStatus.NOT_DELETED)ifrecruit_team:access_token,refresh_tokencreate_tokens(str(recruit_team.id),mobile,enterprise_idstr(recruit_team.enterprise_id))return{enterprise_access_token:access_token,enterprise_refresh_token:refresh_token,enterprise_id:recruit_team.enterprise_id,user_type:team,is_owner:is_owner,mobile:mobile}# 2) 兜底企業負責人資質聯系人 已審核通過foreqinawaitEnterpriseQualification.filter(contact_phonemobile):reviewawaitEnterpriseReview.get_or_none(enterprise_ideq.enterprise_id)ifreviewandreview.review_result1:access_token,refresh_tokencreate_tokens(str(eq.enterprise_id),mobile,roleenterprise,enterprise_idstr(eq.enterprise_id))return{enterprise_access_token:access_token,enterprise_refresh_token:refresh_token,enterprise_id:eq.enterprise_id,user_type:enterprise,is_owner:True,mobile:mobile}raiseBusinessError(該手機號未關聯可用團隊或已認證企業)三、坑 2企業端一直顯示“尚未關聯到企業”現象進入「公司資料」頁提示“尚未關聯到企業”。根因舊 token 的user_id是recruit_team.id團隊成員或enterprise_id企業主前端從 token 里decode出user_id當企業 ID 用 —— 團隊成員賬號直接拿錯壞 token 解不出則顯示未關聯。修復讓 token自包含真實 enterprise_id# app/utils/jwt_util.pydefcreate_tokens(user_id,username,roleNone,enterprise_idNone):payload{user_id:user_id,username:username,type:enterprise}ifrole:payload[role]roleifenterprise_id:payload[enterprise_id]enterprise_id# 關鍵字段...// 前端 src/utils/jwt.jsexportfunctiongetEnterpriseIdFromToken(token){constpayloaddecodeJwtPayload(token)if(!payload)returnnull// 優先讀 token 自帶的 enterprise_id壞 token 也兜得住returnpayload.enterprise_id??payload.user_id??null}并新增后端GET /enterprise/me由后端統一從 token 解析企業上下文替代前端本地解碼enterprise_router.get(/me)asyncdefenterprise_me(infoDepends(get_enterprise_info)):return{code:1,message:查詢成功,data:info}四、坑 3登錄報“驗證碼已過期”實際是 Redis 與吞異常現象輸入正確驗證碼仍報“驗證碼已過期”后端日志卻顯示redis_code462780, input_code111111信息明顯錯亂。根因兩個redis_client寫死db6而.env.dev配置REDIS_DB8驗證碼的寫入與讀取不在同一個 Redis DB。舊登錄邏輯里team_login與enterprise login各校驗一次驗證碼路由再用try/except把第一次錯誤吞掉導致日志與提示對不上且業務錯誤走 HTTP 500前端只顯示“服務器內部錯誤”。修復Redis 客戶端統一讀配置redis_clientredis.Redis(hostsettings.REDIS_HOST,portsettings.REDIS_PORT,dbsettings.REDIS_DB,decode_responsesTrue)新增BusinessError業務異常返回 HTTP 200 code:0message不再污染 500 日志# app/core/exception_handler.pyclassBusinessError(Exception):passapp.exception_handler(BusinessError)asyncdefbusiness_error_handler(request,exc:BusinessError):returnJSONResponse(status_code200,content{code:0,message:str(exc)})驗證碼只校驗一次并區分“已過期 / 錯誤”staticmethoddef_verify_sms_code(mobile,code):ifsettings.debug_modeandcode000000:# dev 萬能碼returnredis_coderedis_client.get(fboss-api:enterprise-login:sms:{mobile})ifredis_codeisNone:raiseBusinessError(驗證碼已過期請重新獲取)ifredis_code!code:raiseBusinessError(驗證碼錯誤)五、坑 4登錄提示“未獲取到有效令牌”現象登錄頁直接報“登錄失敗未獲取到有效令牌”。根因src/api/enterprise.js的enterpriseLogin用的是裸 axios沒走request攔截器返回的是AxiosResponse而Login.vue按攔截器習慣寫成const data res.data把后端統一響應體{code,message,data}當成了內層 token 對象自然取不到enterprise_access_token。修復Login.vue的onSubmit先取res.data作為后端統一響應體再判斷code1取內層dataconstresawaitenterpriseLogin({mobile:form.phone,code:form.code})constresultres.data||{}if(result.code!1){ElMessage.error(result.message||登錄失敗)return}constdataresult.data||{}consttokendata.enterprise_access_tokenif(!token){ElMessage.error(登錄失敗未獲取到有效令牌)return}localStorage.setItem(companyToken,token)localStorage.setItem(companyUserInfo,JSON.stringify({mobile:data.mobile||form.phone,enterprise_id:data.enterprise_id,user_type:data.user_type||team,is_owner:data.is_ownertrue}))awaitrouter.replace(/company/dashboard)經驗裸 axios 與封裝 request 的返回值結構不同調用方必須按實際返回取值。統一約定很重要。六、配套的權限與接口加固1. 鑒權 500 → 401get_enterprise_info在 token 無效/無權限時拋AuthenticationError全局異常處理器返回HTTP 401 code:401前端攔截器據此清 token 并跳登錄// 響應攔截器if(res.code401){localStorage.removeItem(companyToken)localStorage.removeItem(companyUserInfo)router.replace(/login)}2. 企業管理 / 認證審核職責分離account_status1→ 出現在「認證審核」頁注冊的待審信息account_status0/2/3→ 出現在「企業管理」頁已審核正常/封禁/已駁回新增POST /enterprise/ban真實封禁/解封接口3. 職位管理搜索改用 Elasticsearch/job/list在有關鍵字/城市時走 ESboss_job_index_v2ik_max_word 中文分詞ES 異常自動降級到 MySQL 模糊查詢保證可用性。索引需先建POST /es-data/create-index-v2 POST /es-data/insert-data-v2七、避坑清單可直接抄Redis DB 一定要讀配置不要硬編碼否則換環境驗證碼讀寫不一致。驗證碼只校驗一次路由層不要 try/except 吞掉上游業務異常。業務錯誤用專用異常BusinessErrorcode:0前端能展示真實 message別用 500。JWT 把必要上下文如 enterprise_id寫進 payload避免前端再去 decode 推斷。前端封裝 request 與裸 axios 返回值結構不同調用處務必統一約定。HTTP 401 清登錄態并跳轉避免壞 token 一直卡在某個頁面。團隊賬號與企業主賬號的 ID 空間可能重合靠role字段區分別用 user_id 硬猜。八、小結登錄看似簡單其實橫跨前端取值、JWT 簽發、Redis 驗證碼、后端鑒權依賴、異常體系五層。今天把這條鏈路從“反復 500 / 跳回登錄 / 未關聯 / 無令牌”四個坑全部填平核心就三件事后端單一 login 一次校驗 BusinessError鑒權role 區分賬號類型 401 清態前端按真實返回結構取值 攔截器統一跳轉。這樣企業端和管理端才真正跑通。