BOSS 直聘仿写项目实战:管理端企业列表与认证审核(前后端闭环)

发布时间:2026/7/29 2:20:12
BOSS 直聘仿写项目实战:管理端企业列表与认证审核(前后端闭环) 本文是 Boss-API 系列的「企业审核篇」承接前两篇三端架构总览、求职者管理与忘记密码。前两篇把「人」侧的业务跑通了本篇聚焦「企业」侧最核心的一块管理端怎么拉企业列表、怎么审核认证、审核结果又怎么回流到企业端。涉及今天真实改过的代码后端enterprise_api.py/enterprise_service.py/models/enterprise.py前端boss-manage-ui的request.js/api/enterprise.js/Companies.vue/CertificationList.vue/CertificationAudit.vue以及企业端boss-company-ui的认证页。所有代码均来自联调现场含踩坑记录。一、这个 feature 要解决什么一个招聘平台得让企业来「认证」。整条链路是企业端提交认证资料 ── 写「企业信息表」(t_enterprise_info) │ ▼ 管理端看到待审企业列表 ── 审核员点开详情、看资质材料 │ ▼ 审核通过 / 驳回 ── 写「企业审核表」(t_enterprise_review) 更新企业主表状态 │ ▼ 审核结果回流企业端 ── 企业登录后在认证页看到「已通过 / 已驳回 原因」这里有两张关键表职责完全不同一定要分清楚这也是今天联调时反复澄清的点表谁写什么时候写作用t_enterprise_info企业信息表企业端企业提交认证申请时存营业执照、法人、注册资本等工商资料t_enterprise_review企业审核表管理端审核员点「通过 / 驳回」时存审核结果、驳回原因、审核时间前端页面和它们的关系管理端「企业管理 / 企业认证审核」列表展示的是审核视角的数据管理端审核操作落库到审核表企业端只负责填资料、查自己这份审核结果。二、后端四张表与账号状态枚举1. 模型app/models/enterprise.py一张企业主表 三张扩展表主表用enterprise_id关联其余三张当前实现是IntField关联不是外键约束灵活但要求代码里自己判空classAccountStatus(IntEnum):账号状态NORMAL0# 正常PENDING_AUDIT1# 待审核BANNED2# 封禁REJECTED3# 已驳回审核不通过classEnterprise(Model):企业表主表idfields.IntField(pkTrue)enterprise_namefields.CharField(max_length255)enterprise_codefields.CharField(max_length100,uniqueTrue)# 企业编码account_statusfields.IntEnumField(enum_typeAccountStatus)# 0/1/2/3audit_typefields.IntEnumField(enum_typeAuditType)# 1新认证/2资质更新/3信息变更auth_timefields.DatetimeField(nullTrue)submit_timefields.DatetimeField(nullTrue)# 企业提交认证时间# ... 其余字段classEnterpriseInfo(Model):企业信息表营业执照 / 法人 / 注册资本 / 行业 / 总部地址 ...enterprise_idfields.IntField(nullTrue)unified_social_credit_codefields.CharField(max_length50,uniqueTrue)legal_representativefields.CharField(max_length100)industryfields.ForeignKeyField(models.IndustryPosition,related_nameenterprise_info_list)headquarters_addressfields.CharField(max_length512)# ...classEnterpriseQualification(Model):资质材料表营业执照图片 / 法人身份证正反面 / 联系人 ...enterprise_idfields.IntField(nullTrue)business_license_urlfields.CharField(max_length512,nullTrue)legal_id_front_urlfields.CharField(max_length512,nullTrue)legal_id_back_urlfields.CharField(max_length512,nullTrue)# ...classEnterpriseReview(Model):企业审核表审核结果 / 驳回原因 / 审核时间 / 审核人enterprise_idfields.IntField(nullTrue)review_resultfields.IntField(nullTrue)# 1通过 / 2驳回review_reasonfields.CharField(max_length512,nullTrue)audit_timefields.DatetimeField(nullTrue)audit_userfields.CharField(max_length100,nullTrue)重点AccountStatus有 4 个值0/1/2/3。今天联调时踩过一个大坑——审核驳回后前端枚举只画了 0/1/2 三态导致「已驳回」的企业在列表/详情里状态显示成「未知」。枚举一加全前端映射也要同步加3: 已驳回。2. 三个核心接口app/apis/enterprise_api.pyenterprise_routerAPIRouter(prefix/enterprise,tags[企业管理])enterprise_router.get(/list)# 企业列表管理端/审核页共用asyncdefselect_enterprise_list(page:intQuery(1,ge1),page_size:intQuery(10,ge10,le50),# 注意约束最小 10、最大 50enterprise_name:strQuery(None),submit_time_start:strQuery(None),submit_time_end:strQuery(None),):resawaitEnterpriseService.select_enterprise_list(...)return{code:1,message:查询成功,data:res}enterprise_router.get(/detail/{enterprise_id})# 企业详情含审核记录asyncdefselect_enterprise_by_id(enterprise_id:int):...enterprise_router.post(/review)# 企业审核通过/驳回asyncdefenterprise_review(req:EnterpriseReviewCreateRequest):awaitEnterpriseService.enterprise_review(req)return{code:1,message:审核完成}返回统一用code: 1表示成功——和求职者端一致但和前两篇里管理端求职者查询用的code: 200不一样。这点下文明坑。3. 列表查询enterprise_service.py列表接口要做三件事分页、按需关联三张扩展表、把行业名算出来。关键在industry必须判空否则缺资料的企业会 500staticmethodasyncdefselect_enterprise_list(page,page_size,enterprise_name,submit_time_start,submit_time_end):queryEnterprise.all()ifenterprise_name:queryquery.filter(enterprise_name__icontainsenterprise_name)ifsubmit_time_start:queryquery.filter(submit_time__gtesubmit_time_start)ifsubmit_time_end:queryquery.filter(submit_time__ltesubmit_time_end)total_countawaitquery.count()total_pagemath.ceil(total_count/page_size)rowsawaitquery.offset((page-1)*page_size).limit(page_size)enterprise_list[]forenterpriseinrows:infoawaitEnterpriseInfo.get_or_none(enterprise_identerprise.id).prefetch_related(industry)qualawaitEnterpriseQualification.get_or_none(enterprise_identerprise.id)# 行业名info 或 info.industry 任一为空都不能直接 .nameindustry_nameinfo.industry.nameif(infoandgetattr(info,industry,None))elseNoneenterprise_list.append({enterprise:enterprise,enterpriseinfo:info,enterprise_qualification:qual,industry:industry_name,})return{total_count:total_count,total_page:total_page,page:page,page_size:page_size,enterprise_list:enterprise_list,}前端真正用到的就这几个字段total_count / total_page / page / page_size分页enterprise_name / enterprise_code主表industry行业名enterpriseinfo.headquarters_address总部地址。4. 审核落库enterprise_review—— 写入「企业审核表」这是「审核成功后将数据写企业审核表」的那一步。注意它同时维护审核表和企业主表状态staticmethodasyncdefenterprise_review(req:EnterpriseReviewCreateRequest):enterpriseawaitEnterprise.get_or_none(idreq.enterprise_id)ifenterpriseisNone:raiseException(f企业不存在(enterprise_id{req.enterprise_id}))reviewawaitEnterpriseReview.get_or_none(enterprise_idreq.enterprise_id)ifreviewisNone:awaitEnterpriseReview.create(enterprise_idreq.enterprise_id,review_resultreq.review_result,review_reasonreq.review_reason,remarkreq.remark,audit_timenow(),)else:review.review_resultreq.review_resultorreview.review_result review.review_reasonreq.review_reasonorreview.review_reason review.remarkreq.remarkorreview.remark review.audit_timenow()awaitreview.save()ifreq.review_result1:# 通过enterprise.account_statusAccountStatus.NORMAL enterprise.auth_timenow()elifreq.review_result2:# 驳回enterprise.account_statusAccountStatus.REJECTEDawaitenterprise.save()审核通过 → 主表变「正常」且写认证时间审核驳回 → 主表变「已驳回」。审核结果本身落在t_enterprise_review。前端拿detail接口里的review字段就能知道这家企业处于什么审核状态。三、前端从「写死 mock」到「接真实接口」管理端是 Vue3 Element Plus Vite4请求统一走封装好的request.js由 Vite 的/api代理转发到后端 8000前两篇已讲这里不再展开代理配置。1. 第一步永远是「成功码对齐」utils/request.js今天第一坑响应拦截器原来写if (res.code ! 200) reject(...)但企业接口返回的是code: 1于是所有企业相关请求都被当成失败列表永远拿不到数据。改成和后端约定一致service.interceptors.response.use((response){constresresponse.data// 后端业务约定code 1 表示成功if(res.code!1){if(res.code401){/* 跳登录 */}elseElMessage.error(res.message||请求失败)returnPromise.reject(newError(res.message||Error))}returnres})经验前后端成功码必须一对一定死。本项目里求职者接口、企业接口用code:1前两篇的管理端求职者查询用code:200——同一套前端不同模块约定不同全靠request.js拦截器统一兜底别在业务页面里各写各的。2. 抽一层 APIapi/enterprise.js所有企业接口集中在一个文件页面只调语义化方法不关心 URLimportrequestfrom/utils/requestexportfunctiongetEnterpriseList(params){returnrequest({url:/enterprise/list,method:get,params})}exportfunctiongetEnterpriseDetail(enterpriseId){returnrequest({url:/enterprise/detail/${enterpriseId},method:get})}exportfunctionenterpriseReview(data){returnrequest({url:/enterprise/review,method:post,data})}3. 企业列表页Companies.vue/CertificationList.vue「企业管理」和「企业认证审核」两个列表页都接入同一个/enterprise/list。进页面onMounted就拉数据映射后端返回的嵌套结构constfetchListasync(){loading.valuetruetry{constresawaitgetEnterpriseList({page:query.page,page_size:query.page_size,enterprise_name:query.enterprise_name||undefined,submit_time_start:query.submit_time_start||undefined,submit_time_end:query.submit_time_end||undefined,})constdatares.data tableData.valuedata.enterprise_list||[]// 嵌套数组pagination.totaldata.total_count pagination.currentPagedata.page pagination.pageSizedata.page_size}finally{loading.valuefalse}}onMounted(fetchList)表格列直接吃后端的字段el-table-columnlabel企业名称template#default{ row }{{ row.enterprise.enterprise_name }}/template/el-table-columnel-table-columnlabel企业编码template#default{ row }{{ row.enterprise.enterprise_code }}/template/el-table-columnel-table-columnlabel行业template#default{ row }{{ row.industry || - }}/template/el-table-columnel-table-columnlabel总部地址template#default{ row }{{ row.enterpriseinfo?.headquarters_address || - }}/template/el-table-column注意row.enterpriseinfo?.headquarters_address用了可选链——因为enterpriseinfo可能为null企业没填资料时。这和后端industry判空是一对前后端都要兜底。4. 审核详情页CertificationAudit.vue—— 审核操作 回执点列表里的「审核」路由带enterprise_id跳到详情页先拉detail接口再展示资质材料营业执照 / 法人身份证正反面用el-image预览和「当前审核记录」横幅来自review字段。通过 / 驳回后不再直接跳走而是展示一张回执卡consthandleApproveasync(){awaitElMessageBox.confirm(确定要通过企业${detail.enterprise.enterprise_name}的认证申请吗,审核通过确认)awaitenterpriseReview({enterprise_id:Number(route.params.id),review_result:1,// 通过remark:auditForm.remark||undefined,})buildReceipt(通过)// 展示回执操作/企业/时间 已通知企业}驳回类似多带review_reason。四、打通「管理端审核 → 企业端查看」闭环审核不是管理端自嗨结果得让企业端看到。链路是企业端提交认证 →t_enterprise_info落库主表account_status 1待审核管理端审核通过/驳回 →t_enterprise_review落库 主表状态变0/3企业端登录后认证页用 JWT 里的enterprise_id调GET /enterprise/detail/{id}读review.review_result和account_status决定显示什么// boss-company-ui/.../Certification.vueconstfetchAuthStatusasync(){constidgetEnterpriseIdFromToken()// JWT payload.user_id enterprise_idconst{data}awaitgetEnterpriseDetail(id)constrdata.reviewif(rr.review_result2)showRejected(r.review_reason)// 已驳回 原因elseif(data.enterprise.account_status0||(rr.review_result1))showApproved(data.enterprise.auth_time)elseif(data.enterprise.account_status1)showPending()// 待审核}这样企业被驳回后重新提交、被通过后立即看到认证时间整条审核业务就闭环了。五、本期踩坑合集全是真事成功码不一致所有请求被当失败拦截器按code ! 200判错但企业接口返回code: 1列表永不渲染。前后端成功码必须约定一致本项目企业/求职者接口用1管理端另一查询用200靠拦截器统一兜底。关联查询取到 None 还取属性 → 500enterpriseinfo.industry.name在「缺企业信息 / 行业没关联」时直接AttributeError。列表和详情都要先判空industry_name info.industry.name if (info and info.industry) else None。前端对应用row.enterpriseinfo?.xxx可选链。审核驳回状态没进枚举 → 前端显示「未知」AccountStatus加了REJECTED 3但前端状态映射只画了 0/1/2已驳回企业状态列变「未知」。枚举一改前端三处映射列表 / 详情 / 企业端都要同步加3: 已驳回。孤儿数据导致审核接口 500企业主表记录丢失只剩审核表孤儿记录Enterprise.get_or_none(id)返回None后面enterprise.account_status ...直接AttributeError。修复在enterprise_review/detail里把get_or_none提前、企业不存在就raise Exception(企业不存在)不再触发空指针并清理了孤儿审核记录。列表页残留 mock 数据数据「全部不对」CertificationList.vue企业认证审核列表一度还是写死的假数组auditList里 “某某信息科技有限公司” 之类从不调接口所以页面数据全是假的。排查顺序先看页面用的是不是真实接口、再看接口成功码、再看字段映射、最后看是否 mock 没清。分页参数约束后端page_size约束ge10, le50前端默认 10、可选 [10,20,50]。前端若传 1 或 100 会被后端Query校验直接拒列表拉不到分页。六、整条链路串一遍【企业提交】企业端填表 → POST /enterprise/save → t_enterprise_info t_enterprise_qualification主表 account_status1(待审核) ↓ 【管理端看列表】GET /enterprise/list?pageenterprise_namesubmit_time_* → 分页列表真实数据 ↓ 【管理端审核】点「审核」→ GET /enterprise/detail/{id} 看资质材料 → POST /enterprise/review {review_result:1|2} ↓ 后端写 t_enterprise_review 主表 account_status0(通过)/3(驳回) ↓ 【企业端查看】企业登录 → GET /enterprise/detail/{id} → 读 review.review_result / account_status ↓ 展示已通过(认证时间) / 已驳回(原因) / 待审核七、小结企业侧审核功能本质是「两表分工 一个状态机」两表分工企业信息表t_enterprise_info由企业端写、存资料企业审核表t_enterprise_review由管理端写、存结果。一个状态机企业主表account_status在待审核(1) → 正常(0) / 已驳回(3)之间流转封禁(2) 是运营后续动作。落到代码上还是前两篇反复说的那套范式接口成功码对齐 → 关联查询判空兜底 → 枚举前后端同步 → mock 数据必须清零 → 孤儿/不一致数据提前拦。把企业列表和审核这个闭环吃透一个招聘平台「企业侧」最硬的一块骨头就啃下来了。本文基于真实 FastAPI Vue3 项目整理敏感密钥已脱敏。FastAPI / Tortoise-ORM / Element Plus / Vite 文档以官方最新版为准。