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 清态前端按真实返回结构取值 拦截器统一跳转。这样企业端和管理端才真正跑通。

相关新闻

我们开源了一套让 LLM 真正“学会思考”的方法论

我们开源了一套让 LLM 真正“学会思考”的方法论

Agentic Method —— 不是更多的 prompt,而是一套让模型输出更可观测、可证伪、可进化的工作流。 GitHub: https://github.com/Ghost-Silver/Agentic-Method一个越来越明显的问题 现在的 LLM,尤其是中档模型,指令遵循能力已经很强了。你让它按…

2026/7/30 3:51:46 阅读更多
Python生成器原理与应用:从yield到内存优化

Python生成器原理与应用:从yield到内存优化

1. 从函数到生成器的跨越第一次遇到yield关键字时,我正尝试处理一个超过10GB的日志文件。传统方法是将整个文件读入内存,结果自然是内存溢出。同事建议我试试生成器,从此打开了新世界的大门。yield是Python中一个神奇的关键字,它能…

2026/7/31 5:48:30 阅读更多
Jetpack Compose滚动布局进阶:LazyListState控制与性能优化实战

Jetpack Compose滚动布局进阶:LazyListState控制与性能优化实战

1. 项目概述:从“能用”到“好用”的滚动体验在移动应用开发中,滚动布局是用户交互的基石。无论是浏览社交动态、查看商品列表,还是阅读长篇文章,流畅、自然的滚动体验直接决定了用户对应用的第一印象和留存意愿。Jetpack Compose…

2026/7/30 3:51:46 阅读更多
STM32开发必知:CubeMX沙箱段配置与链接脚本实战解析

STM32开发必知:CubeMX沙箱段配置与链接脚本实战解析

1. 从“沙箱”说起:CubeMX配置的基石逻辑刚接触STM32和CubeMX的朋友,第一次看到“沙箱段”这个词,多半会有点懵。这名字听起来像是某种隔离的安全区域,和芯片配置有什么关系?我第一次在CubeMX的工程设置里看到它时&…

2026/7/31 5:45:00 阅读更多
程序员攻克技术英语:词根词缀实战指南与编程术语解析

程序员攻克技术英语:词根词缀实战指南与编程术语解析

1. 为什么程序员必须啃下词根词缀这块硬骨头“程序员也要学英语”,这话听起来像一句正确的废话,但真正能把它当回事,并且找到高效路径的人,少之又少。很多人一提到学英语,脑子里蹦出来的就是背单词、刷美剧、练口语&am…

2026/7/31 5:45:00 阅读更多
HART协议详解:05 HART现场通信实战

HART协议详解:05 HART现场通信实战

第五季 HART现场通信实战 ——从USB-HART Modem抓包到工程诊断:让协议知识变成维修能力 各位工业现场的工程师朋友们,大家好! 经过前四季的系统学习,我们已经构建了HART协议的完整理论框架: 第一季:六层生命模型与本质认知 第二季:物理层4–20mA与FSK魔法 第三季:数…

2026/7/31 0:14:40 阅读更多
维修工程师的示波器实战:02 探头地线——示波器最大的“坑”

维修工程师的示波器实战:02 探头地线——示波器最大的“坑”

第二篇:探头地线——示波器最大的“坑” ——那根不起眼的小地线,可能比你测的信号还重要 很多工程师第一次用示波器时,都会经历这样一个“惊魂”时刻。 某食品厂包装线,伺服偶发报警。年轻工程师判断是编码器信号受干扰,便拿出示波器认真测量。波形一出来,所有人都倒…

2026/7/31 0:14:40 阅读更多