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 工具。

相关新闻

Maven Surefire与Failsafe插件实现单元测试与集成测试分离的完整指南

Maven Surefire与Failsafe插件实现单元测试与集成测试分离的完整指南

1. 项目概述:为什么我们需要分离单元测试与集成测试?在Java后端开发中,测试是保证代码质量的基石。但很多团队,尤其是项目初期,往往会把所有测试一股脑地扔给Maven Surefire插件去执行。这会导致一个典型问题&#xff…

2026/7/29 3:26:31阅读更多 →
自注意力机制原理与Transformer模型实践指南

自注意力机制原理与Transformer模型实践指南

1. 从生活场景理解自注意力机制想象你正在阅读一本侦探小说,主角在犯罪现场发现了几十个线索。普通人的做法可能是按顺序逐个查看每个线索,但侦探会怎么做?他会快速扫视所有线索,然后:重点关注带血的匕首(相…

2026/7/29 3:26:31阅读更多 →
Qt6自定义QML组件加载失败:从原理到CMake配置的完整解决方案

Qt6自定义QML组件加载失败:从原理到CMake配置的完整解决方案

1. 从一次“文件找不到”的报错说起 最近在把项目从 Qt5 升级到 Qt6 的过程中,我遇到了一个挺典型的坑:在尝试加载一个自定义的 QML 文件时,程序直接崩溃了,控制台抛出的错误信息是 file:///.../MyCustomComponent.qml: No such …

2026/7/29 3:26:31阅读更多 →
Tkinter事件驱动编程:从静态界面到动态交互的完整指南

Tkinter事件驱动编程:从静态界面到动态交互的完整指南

1. 从“会画”到“会动”:Tkinter事件驱动编程的核心如果你已经跟着上一篇教程,用Tkinter的Label、Button、Entry等基础控件搭出了一个静态的界面,那么恭喜你,你已经成功迈出了GUI编程的第一步。但很快你就会发现,一个…

2026/7/29 4:41:12阅读更多 →
Logisim存储器设计:从地址译码到Cache映射的计算机组成原理实践

Logisim存储器设计:从地址译码到Cache映射的计算机组成原理实践

1. 从理论到实践:为什么我们要在Logisim里“造”存储器?如果你正在学习计算机组成原理或者硬件系统设计,尤其是像华中科技大学这类顶尖高校的课程,那么“用Logisim实现一个存储器系统”这个实验,大概率是你绕不过去的一…

2026/7/29 4:41:12阅读更多 →
VLC媒体播放器转码功能实战:从基础操作到高效批量处理

VLC媒体播放器转码功能实战:从基础操作到高效批量处理

VLC媒体播放器转码功能实战:从基础操作到高效批量处理 【免费下载链接】vlc VLC media player - plays everything, runs anywhere. Code here: https://code.videolan.org/videolan/vlc 项目地址: https://gitcode.com/gh_mirrors/vl/vlc VLC媒体播放器不仅…

2026/7/29 4:41:12阅读更多 →
Dev C++ Socket编程与ws2_32库链接:从实践到C/C++面试系统复习

Dev C++ Socket编程与ws2_32库链接:从实践到C/C++面试系统复习

1. 项目概述:从Dev C Socket编程到面试复习的系统路径最近在带几个刚入行的C/C新人,发现一个挺普遍的现象:很多人为了准备面试,会去网上找一堆“八股文”来背,什么虚函数表、智能指针、设计模式,背得滚瓜烂…

2026/7/29 4:41:12阅读更多 →
ESP32-S3 Arduino开发环境搭建全攻略:从零到点灯避坑指南

ESP32-S3 Arduino开发环境搭建全攻略:从零到点灯避坑指南

1. 项目概述:为什么ESP32-S3值得你花时间搭建环境?如果你正在寻找一款性能强劲、接口丰富且性价比极高的物联网开发板,ESP32-S3绝对是一个绕不开的选择。作为乐鑫在ESP32系列中的“性能担当”,它集成了双核Xtensa LX7处理器、主频…

2026/7/29 4:41:12阅读更多 →
C++命名空间、缺省参数与函数重载:从基础概念到工程实践

C++命名空间、缺省参数与函数重载:从基础概念到工程实践

1. 从“Hello World”到真正的C&#xff1a;为什么我们需要命名空间、缺省参数和函数重载&#xff1f;很多朋友学C&#xff0c;都是从经典的“Hello World”开始的。你照着教程&#xff0c;写下#include <iostream>和using namespace std;&#xff0c;然后一个cout <&…

2026/7/29 4:39:11阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

&#x1f539; 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具&#xff0c;凭借本地离线运行、可视化图形操作和任务自动化三大核心特性&#xff0c;赢得了众多用户的青睐。与普通在线对话AI工具不同&#xff0c;它属于能够直接操控本机软硬件的智能数字员工…

2026/7/28 4:06:39阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接&#xff0c;是用激光束对阀座壳体&#xff08;通常为不锈钢或铝合金&#xff09;进行密封焊接&#xff0c;使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/28 2:08:06阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX&#xff1a;三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/28 1:38:28阅读更多 →
28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停&#xff1f;用 interrupt 给它设个“关卡“&#xff01; 在构建复杂的 Agent 系统时&#xff0c;我们经常会遇到这样的场景&#xff1a;Agent 正在执行一个多步骤的任务&#xff0c;比如“下单购买商品”&#xff0c;但执行到一半时&#xff0c;我们…

2026/7/29 0:01:46阅读更多 →
自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

近日&#xff0c;国际专注开放式技术研发的声学品牌Nank南卡&#xff0c;正式官宣实力艺人曾舜晞担任品牌代言人。消息一经发出便轰动全网。为什么耳机品牌不选择流量明星、老牌歌手&#xff1f;而且是选择曾舜晞&#xff1f;让我们一起来探索一下&#xff01;比起短期的流量&a…

2026/7/29 0:01:46阅读更多 →
【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

一、本文介绍 🔥本文在RT-DETR多模态融合目标检测中引入RLAB残差线性注意力模块,可在不同模态特征交互阶段进行多次残差细化,使可见光、红外等特征在尺度、语义和空间位置上更好对齐;随后将细化特征与解码器输出拼接并生成Q、K、V,通过线性注意力自适应强化关键通道、目…

2026/7/29 0:01:46阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时&#xff0c;发现推理速度只有可怜的 1-2 FPS&#xff0c;而别人的演示视频却能跑到 30 FPS 以上&#xff0c;那么问题很可能不在模型本身&#xff0c;而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后&#xff0c;会直接使用官方示例…

2026/7/28 20:22:24阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一&#xff1a;为什么你需要了解 Coze 和 Dify&#xff1f;如果你对 AI 应用开发感兴趣&#xff0c;但一看到“大模型”、“智能体”、“工作流”这些词就头疼&#xff0c;觉得门槛太高&#xff0c;那这篇文章就是为你准备的。很多开发者&#xff0c;包括我自己&#…

2026/7/29 4:31:51阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会&#xff1a;配图一直是个让人头疼的问题。2026年&#xff0c;AI生图工具已经非常成熟了&#xff0c;但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1&#xff1a;速度之王2026年6月11日&#xff0c…

2026/7/28 2:35:58阅读更多 →