FastAPI + Tortoise-ORM 企业端实战:企业认证、信息保存、审核与登录的设计与实现

发布时间:2026/7/29 3:28:33

FastAPI + Tortoise-ORM 企业端实战:企业认证、信息保存、审核与登录的设计与实现
FastAPI Tortoise-ORM 企业端实战企业认证、信息保存、审核与登录的设计与实现一、前言介绍1.1 项目背景1.2 功能概览1.3 数据模型总览二、环境准备2.1 依赖库清单2.2 数据库与缓存配置要点2.3 路由与目录结构三、知识点讲解3.1 企业域四表拆分与 enterprise_id 关联3.2 枚举驱动的状态机3.3 Form 依赖注入表单 文件混合提交3.4 多文件 OSS 上传与事务边界3.5 列表分页与关联预加载四、代码逻辑拆解4.1 保存企业信息事务 三文件 三表4.2 企业列表分页 筛选 关联4.3 企业详情4.4 企业审核通过改状态4.5 企业登录手机号验证码 审核态 JWT一、前言介绍1.1 项目背景本项目是一个仿 BOSS 直聘的招聘平台除求职者端之外还包含企业招聘方端。企业端的核心闭环是企业提交认证材料 → 平台审核 → 审核通过后企业可用手机号登录。本文聚焦企业域的四张表与五个接口逐行拆解其设计与实现。1.2 功能概览保存企业信息一次性提交企业工商信息 三份资质文件营业执照、法人身份证正反面落三张表。企业列表分页 按名称模糊、按提交时间区间筛选列表项聚合主表 / 信息表 / 资质表 / 行业名称。企业详情按 ID 查出完整企业档案。企业审核对某企业标记通过 / 驳回审核通过时把主表账号状态置为正常。企业登录用资质表里登记的手机号 短信验证码登录前提是该企业已审核通过成功后签发双 Token。1.3 数据模型总览Enterprise企业主表名称、Code、账号状态、认证类型、风险、黑名单、审核类型 │ enterprise_idIntField 关联非外键 ├── EnterpriseInfo企业信息表信用代码、法人、资本、行业、规模、融资、经营范围 ├── EnterpriseQualification资质材料表联系人、三文件 URL、组织机构代码证 └── EnterpriseReview企业审核表结果、原因、备注、审核人、时间关键点四张表通过enterprise_id这个整数字段手动关联而不是用ForeignKeyField串起来模型里外键写法被注释掉了。这是本模块最值得讲的设计取舍详见问题排查 5.1。二、环境准备2.1 依赖库清单tortoise-ormaerich提供异步 ORM 功能与数据库迁移支持。pydantic用于请求参数校验与表单Form数据验证。python-multipart由于“保存企业信息”接口需要同时接收表单字段与文件此库为必需依赖。oss2用于将三份企业资质文件上传并存储至阿里云 OSS。redis用于存储企业登录所需的短信验证码并指定使用 Redis 的db6进行隔离。2.2 数据库与缓存配置要点redis_clientredis.Redis(hostlocalhost,port6379,db6,decode_responsesTrue)企业登录验证码单独落在 Redisdb6与简历域字典树缓存db2隔离避免键空间互相污染发送验证码的一端也必须写db6否则读写错位验证码永远取不到见问题排查 5.6。2.3 路由与目录结构企业端同样遵循分层约定app/ ├── models/enterprise.py # 四张表 全部枚举定义 ├── schemas/enterprise.py # 保存/审核/登录请求体 ├── apis/enterprise_api.py # /enterprise 路由组 ├── services/enterprise_service.py # 企业业务逻辑 └── core/depends.py # get_enterprise_form表单依赖路由统一挂在prefix/enterprise的APIRouter下。三、知识点讲解3.1 企业域四表拆分与 enterprise_id 关联企业档案天然分成身份状态“工商信息”“资质材料”审核流水四部分用四张表承载classEnterprise(Model):enterprise_codefields.CharField(max_length100,uniqueTrue,description企业Code)account_statusfields.IntEnumField(enum_typeAccountStatus,...)# ... 状态相关字段classEnterpriseInfo(Model):unified_social_credit_codefields.CharField(max_length50,uniqueTrue,...)enterprise_idfields.IntField(nullTrue,description企业ID)# ... 工商信息字段classEnterpriseQualification(Model):enterprise_idfields.IntField(nullTrue,description企业ID)# ... 资质字段classEnterpriseReview(Model):enterprise_idfields.IntField(nullTrue,description企业ID)# ... 审核字段主表Enterprise存账号状态类数据其余三张表各存一类信息都通过enterprise_id整型指回主表模型里原本有ForeignKeyField一对一的写法被注释掉了改用IntField手动关联——好处是建表没有外键约束、迁移简单代价是跨表关系要业务代码自己维护见 5.1。3.2 枚举驱动的状态机账号状态、认证类型、审核类型等都用IntEnum表达保证数据库里存的是数字、代码里读的是语义classAccountStatus(IntEnum):NORMAL0# 正常PENDING_AUDIT1# 待审核BANNED2# 封禁classAuthType(IntEnum):NO_AUTH0ENTERPRISE_AUTH1LICENSE_AUTH2ENTERPRISE_LICENSE_AUTH3新建企业时直接写account_statusAccountStatus.PENDING_AUDIT自文档化避免魔法数字审核通过后把状态改成AccountStatus.NORMAL状态流转清晰可追溯。3.3 Form 依赖注入表单 文件混合提交保存企业信息既要传一堆表单字段又要传三个文件请求体是multipart/form-data。这里用Form 依赖把字段重组回 Pydantic 模型asyncdefsaveEnterpriseInfo(form:EnterpriseCreateRequestDepends(get_enterprise_form),business_license_file:UploadFileFile(None,...),legal_id_front_file:UploadFileFile(None,...),legal_id_back_file:UploadFileFile(None,...)):asyncdefget_enterprise_form(enterprise_nameForm(...,description企业名称),unified_social_credit_codeForm(...,description统一社会信用代码),# ... 其余字段逐个 Form(...)):returnEnterpriseCreateRequest(enterprise_nameenterprise_name,...)路由把结构化字段交给Depends(get_enterprise_form)由它把十几个Form(...)参数组装成EnterpriseCreateRequest文件则单独用File(...)接收这种写法让表单字段和文件在路由里各司其职避免手写解析request.form()。3.4 多文件 OSS 上传与事务边界保存企业涉及三张表写入 三份文件上传天然需要事务保证一致性asyncwithin_transaction()asconn:enterpriseawaitEnterprise.create(...)# 主表awaitEnterpriseInfo.create(...,enterprise_identerprise.id)# 信息表# 三份文件逐个读 上传 OSS# 资质表 create带上三个 access_urlin_transaction()保证三张表要么全写、要么全滚但 OSS 上传发生在数据库事务之内它并不受数据库回滚约束见 5.2。3.5 列表分页与关联预加载列表接口要分页、要按条件筛选、还要把行业名称带出来query_enterpriseEnterprise.all()ifenterprise_name:query_enterprisequery_enterprise.filter(enterprise_name__icontainsenterprise_name)total_countawaitquery_enterprise.count()offset(page-1)*page_size query_enterpriseawaitquery_enterprise.offset(offset).limit(page_size)icontains做名称模糊匹配、gte/lte做时间区间先count()拿总数算总页数再offset().limit()取当页是标准游标分页写法。四、代码逻辑拆解4.1 保存企业信息事务 三文件 三表asyncwithin_transaction()asconn:enterpriseawaitEnterprise.create(enterprise_nameform.enterprise_name,enterprise_codestr(uuid.uuid4()),account_statusAccountStatus.PENDING_AUDIT,blacklist_statusBlackListStatus.NOT_BANNED,audit_typeAuditType.NEW_ENTERPRISE_AUTH,submit_timenow(),auth_typeAuthType.ENTERPRISE_LICENSE_AUTH,)第 1 行开启数据库事务后续写操作都走这条连接第 3 行enterprise_code用uuid.uuid4()生成全局唯一码作为企业对外标识对外暴露 UUID 比暴露自增 ID 更安全第 4–7 行新建即进入待审核状态、未拉黑、审核类型为新企业认证并打上提交时间——状态机初始态在创建时一次性定好。awaitEnterpriseInfo.create(enterprise_nameform.enterprise_name,unified_social_credit_codeform.unified_social_credit_code,# ... 工商字段industry_idform.industry_id,enterprise_identerprise.id,)信息表用enterprise_identerprise.id指回主表注意传的是整型 id不是 ORM 对象因为这里刻意没用外键industry_id直接存行业字典的 ID行业名称在列表/详情时再prefetch_related取。business_license_file_file_contentawaitbusiness_license_file.read()ossAliyunOSSTool()is_success,success_resoss.upload_single_file(business_license_file_file_content,business_license_file.filename,oss_pathenterprise/,)ifnotis_success:raiseException(上传营业执照图片失败)三份文件逻辑完全一致先await file.read()读成 bytes异步必须 await再调 OSS 工具上传到enterprise/目录失败直接抛错抛错会让in_transaction()回滚前面两张表的写入但已传成功的 OSS 文件回不去见 5.2。awaitEnterpriseQualification.create(contact_nameform.contact_name,contact_phoneform.contact_phone,contact_emailform.contact_email,enterprise_identerprise.id,business_license_urlsuccess_res[access_url],legal_id_front_urlsuccess_res2[access_url],legal_id_back_urlsuccess_res3[access_url],)资质表把三份文件的access_url与联系人信息一起入库enterprise_id同样手动关联主表。4.2 企业列表分页 筛选 关联enterpriseinfoawaitEnterpriseInfo.get_or_none(enterprise_identerprise_id).prefetch_related(industry)res_dict{enterprise:enterprise,enterpriseinfo:enterpriseinfo,enterprise_qualification:enterprise_qualification,industry:enterpriseinfo.industry.name,}get_or_none(enterprise_id...)按整型 id 查信息表再用.prefetch_related(industry)把行业对象一次性加载进来列表项把主表、信息表、资质表、行业名称聚合成一个字典返回前端一次拿到展示所需全部数据注意enterpriseinfo.industry.name这行若enterpriseinfo为None访问.industry会直接抛AttributeError见 5.3。4.3 企业详情enterpriseawaitEnterprise.get_or_none(identerprise_id)enterpriseinfoawaitEnterpriseInfo.get_or_none(enterprise_identerprise_id).prefetch_related(industry)enterprise_qualificationawaitEnterpriseQualification.get_or_none(enterprise_identerprise_id)return{enterprise:enterprise,enterpriseinfo:enterpriseinfo,enterprise_qualification:enterprise_qualification,industry:enterpriseinfo.industry.name,}详情是列表逻辑的精简版按 ID 查主表 信息表带行业 资质表三表聚合返回同样存在 4.2 提到的enterpriseinfo为空时的隐患。4.4 企业审核通过改状态enterprise_reviewawaitEnterpriseReview.get_or_none(enterprise_identerprise_id)ifenterprise_reviewisNone:awaitEnterpriseReview.create(enterprise_identerprise_id,review_resultenterpriseReviewCreateRequest.review_result,review_reasonenterpriseReviewCreateRequest.review_reason,remarkenterpriseReviewCreateRequest.remark,review_timenow(),)else:enterprise_review.review_resultenterpriseReviewCreateRequest.review_result \ifenterpriseReviewCreateRequest.review_resultelseenterprise_review.review_result# ... 原因、备注同样传了才覆盖enterprise_review.review_timenow()awaitenterprise_review.save()第 1–2 行先查是否已有审核记录没有就新建有就更新实现审核流水可重写第 7–8 行更新用if 新值 else 旧值的写法前端没传的字段保留原值避免被None覆盖注意这里没有用exclude_unset而是显式判断因为审核请求体里review_result是必填Field(...)但原因/备注可空需要区分传了空和没传。ifenterpriseReviewCreateRequest.review_result1:enterpriseawaitEnterprise.get_or_none(identerprise_id)enterprise.account_statusAccountStatus.NORMALawaitenterprise.save()审核结果为 1通过时把主表account_status从待审核改成正常状态机向前推进一步驳回非 1则只留审核记录不改变账号状态企业依旧处于待审核、无法登录。4.5 企业登录手机号验证码 审核态 JWTenterprise_qualificationsawaitEnterpriseQualification.filter(contact_phoneloginMobileRequest.mobile)forenterprise_qualificationinenterprise_qualifications:enterprise_identerprise_qualification.enterprise_id enterprise_reviewawaitEnterpriseReview.get_or_none(enterprise_identerprise_id)ifenterprise_review.review_result1:# 审核通过keyfboss-api:enterprise-login:sms:{loginMobileRequest.mobile}redis_coderedis_client.get(key)ifredis_codeisNone:raiseException(验证码已过期)ifredis_code!loginMobileRequest.code:raiseException(验证码错误)access_token,refresh_tokencreate_tokens(str(enterprise_id),loginMobileRequest.mobile)redis_client.delete(key)return{enterprise_access_token:access_token,enterprise_refresh_token:refresh_token}raiseException(账号未审核通过)第 1 行用资质表登记的contact_phone反查企业一个手机号可能对应多条资质记录所以用遍历第 4 行enterprise_review.review_result 1是登录前提——未审核通过直接跳过循环结束抛账号未审核通过第 5–9 行审核通过的才校验 Redis 短信验证码读db6一致则签发双 Token 并删除验证码防止重放create_tokens(str(enterprise_id), mobile)Token 的user_id实际装的是企业 ID与求职者端共用同一套 JWT 工具。

相关新闻

【工具教程】Send.wang 是一个免安装、跨平台的 P2P 文件传输工具

【工具教程】Send.wang 是一个免安装、跨平台的 P2P 文件传输工具

2026/7/29 3:28:33

根据网站页面信息,Send.wang 是一个免安装、跨平台的 P2P 文件传输工具。它的使用方法很简单,核心是“接头暗号”机制,具体步骤如下:📝 使用方法访问网站:在需要互传文件的两台设备(电脑、手机等…

LLM 推理引擎三强争霸——vLLM vs SGLang vs TensorRT-LLM

LLM 推理引擎三强争霸——vLLM vs SGLang vs TensorRT-LLM

2026/7/29 3:18:33

副标题: 2026 年的 LLM 推理引擎格局已经清晰——vLLM 是生产部署的事实标准,SGLang 在 Agent 和结构化输出场景独占鳌头,TensorRT-LLM 是 NVIDIA 硬件上的性能天花板。本文从架构哲学、调度策略、KV Cache 管理、编译优化到实测性能&#xf…

贝叶斯框架:概念、历史、内容与展望!

贝叶斯框架:概念、历史、内容与展望!

2026/7/29 3:18:33

贝叶斯框架(Bayesian Framework)是一套基于贝叶斯定理(Bayes Theorem)的统计与概率推理体系,其核心思想是将概率解释为“对不确定性的信念程度”,并通过先验知识(Prior)与新观察到的…

等保2.0下交换机安全配置:5大高频漏洞加固与实战指南

等保2.0下交换机安全配置:5大高频漏洞加固与实战指南

2026/7/29 4:18:36

1. 项目概述:为什么交换机安全配置是等保2.0的“咽喉要道”干了这么多年网络运维和安全合规,我越来越觉得,交换机这玩意儿,就像家里的总电闸。平时没人注意它,一旦出问题,整个网络都得“停电”。尤其是在等…

嵌入式Linux内核移植实战:从原厂SDK到定制化开发板

嵌入式Linux内核移植实战:从原厂SDK到定制化开发板

2026/7/29 4:18:36

1. 项目概述:从“能用”到“好用”的跨越拿到一块全新的开发板,看着原厂提供的SDK里那个庞大且“通用”的Linux内核源码包,你是不是既兴奋又有点无从下手?兴奋的是,硬件终于到了,可以开始真正的嵌入式Linux…

STM32嵌入式频谱分析与波形识别:从FFT优化到轻量级AI部署实战

STM32嵌入式频谱分析与波形识别:从FFT优化到轻量级AI部署实战

2026/7/29 4:18:36

1. 项目缘起:为什么要在MCU上做频谱分析?几年前,我在做一个工业设备的状态监测项目,需要实时分析电机轴承的振动信号。当时的第一反应是:上工控机,用LabVIEW或者Python做信号处理,稳当。但实地一…

一维字符型数组+二维整型数组

一维字符型数组+二维整型数组

2026/7/29 4:18:36

目录 一、一维字符型数组 1. 核心特性2. 字符型数组定义3. 字符数组存储空间4. 字符型数组初始化 (1)全部初始化(2)局部初始化(3)默认初始化(省略数组长度) 5. 字符串打印6. 字符串…

STM32与A5000实现物联网安全连接方案

STM32与A5000实现物联网安全连接方案

2026/7/29 4:18:35

1. 硬件选型与安全架构设计在物联网设备开发中,安全连接云端服务是核心需求。我们选择了STM32F215RE作为主控芯片,搭配A5000加密芯片的方案,主要基于以下考量:STM32F215RE的优势:内置硬件加密加速器(AES、D…

服装档口数字化转型:微信抖音双平台运营实战指南

服装档口数字化转型:微信抖音双平台运营实战指南

2026/7/29 4:08:35

1. 服装档口转型的行业背景与痛点去年冬天我去广州十三行调研时,发现一个有趣现象:上午十点的批发市场里,半数档口老板都在同时操作三台手机——一台接微信订单,一台回复抖音咨询,还有一台在拍新款短视频。这种"三…

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

2026/7/28 13:30:18

目标:电脑作为RTSP 服务端,循环推送 H264/H265 视频流; RDK X5 通过 rtsp2display 拉流预览,完全不需要在开发板编译 live555。 提供两套成熟方案: ✅ 方案 A:FFmpeg(最简单,优先推…

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

2026/7/28 16:04:36

一、背景与测试方案 在实际项目交付中,PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及:多源PDF的文件流合并、页面级水印渲染(含透明度混合与图层叠加)、输出文件体积控制。看似简单的操作…

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

2026/7/28 16:04:35

说实话,提到PDF拆分再压缩,我真是被折腾得够呛。 上个月公司年度合同归档,一份300多页的PDF总合同,需要按年份拆分成三个独立文件,再分别压缩到10MB以内方便邮件发送各部门确认。我心想这还不简单?先找个海…

AI会议纪要怎么做?会议录音转文字加自动整理,三个月实测流程

AI会议纪要怎么做?会议录音转文字加自动整理,三个月实测流程

2026/7/29 0:08:23

打工人总是跑不掉要写会议纪要。 我在一家互联网公司,一周至少八场会:产品评审、数据复盘、项目同步、客户沟通,每场一小时起步。 以前的标准流程是开会拼命记→会后凭记忆补→整理成文档发群,结果经常记不全、记错、记串。 大概年…

重庆化龙桥老旧小区改造,怎么搞定夜景照明“不扰居”又能省成本?

重庆化龙桥老旧小区改造,怎么搞定夜景照明“不扰居”又能省成本?

2026/7/29 0:08:23

重庆化龙桥靠着嘉陵江,老小区多,最近几年城市更新做的勤,不少住户都反映过小区夜景亮了是好事,可有的灯太晃眼,半夜拉着窗帘都透光,睡不好觉。还有物业算账,这灯开一整晚,公摊电费蹭…

目标模糊、资源泛滥、进度失控,AI学习计划制定失败的3大隐形陷阱及救急方案

目标模糊、资源泛滥、进度失控,AI学习计划制定失败的3大隐形陷阱及救急方案

2026/7/29 0:08:23

更多请点击: https://codechina.net 第一章:目标模糊、资源泛滥、进度失控,AI学习计划制定失败的3大隐形陷阱及救急方案 目标模糊:学得越勤,离真实能力越远 当学习目标停留在“学会AI”或“搞懂大模型”这类宽泛表述…