BOSS 直聘三端项目:企业端登录鉴权全链路修复实战 关键词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 清态前端按真实返回结构取值 拦截器统一跳转。这样企业端和管理端才真正跑通。