ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

公共人才招聘网后台:需求说明书结构化、PDF解析与RBAC权限落地

公共人才招聘网后台:需求说明书结构化、PDF解析与RBAC权限落地 简介面向电子政务与公共就业服务信息化建设整理的项目文档围绕宁夏公共人才招聘网的后台需求展开适合产品经理、需求分析师、Java 后端开发及参与政府网站招投标的技术人员参考。内容依据人社部相关通知与统一信息分类编码体系梳理了公益性招聘平台的建设目标、三层信息结构、主域名与市县区分站点规划并明确人才资源库、重点企业需求库等数据模块。技术要求部分覆盖 J2EE 架构、AJAXStrutsSpringHibernate、B/S 结构、分布式部署与 7×24 小时运行同时列出 Oracle、SQLServer、MySQL 数据库Weblogic、Tomcat 中间件以及 Webservice 二次开发接口、全文检索、站群管理、权限角色分离、SEO 设置与数据迁移等条款可直接转化为功能清单与验收依据。压缩包内仅 1 个 PDF 文件体积约 110KB轻量便于通读与打印批注。已有 63 人学习适合快速把握政务招聘网站后台的功能边界与建设规范。1. 从一份「网站后台需求说明书.pdf」说起公共人才招聘网后台到底要交付什么甲方把一个盖章的《公共人才招聘网 网站后台需求说明书.pdf》丢过来说按这个做三个月后验收现场开始扯皮审核员说职位审核要三级复核开发说 PDF 里只写了审核通过四个字。这类返工在招聘类后台项目里格外密集因为它的角色比普通电商后台更碎——企业管理员、企业 HR、平台审核员、运营、系统管理员每个角色的数据可见范围都不一样而求职者联系方式这类字段一旦越权暴露就是合规事故。真正有价值的做法是把这份 PDF 当成工程输入而不是文档终点一边用结构化方式重新写一版可解析的需求规格说明书一边把 PDF 反向解析成带编号的需求条目库让每条后台功能都能追到条款、追到接口、追到测试用例。下面按写、解、落、比四步展开。2. 需求规格说明书的结构化写法与 PDF 交付流水线2.1 公共人才招聘网后台的需求边界从角色矩阵到用例动手写字之前先把角色矩阵定死它决定了后面 80% 的接口权限判断。招聘网后台的权限纠葛几乎全部集中在三个问题上企业 HR 能不能看到求职者手机号、审核员能不能直接修改职位正文、运营能不能导出全站投递记录。把这三条先答完剩下的功能清单基本是填空题。角色核心操作数据可见范围是否需要二次认证企业管理员单位认证、子账号管理本企业全部数据是企业 HR发布职位、查看投递本企业本人负责职位否平台审核员职位审核、单位资质审核全站待审数据脱敏后字段是运营管理员数据统计、专题配置全站聚合数据不含联系方式是系统管理员账号、角色、日志全站是矩阵定完需求条目按功能需求 FR 非功能需求 NFR两类分编号。功能需求用FR-模块-序号例如FR-JOB-003表示职位模块第 3 条非功能需求用NFR-开头例如并发登录数、职位列表首屏响应时间、日志留存天数。编号必须唯一且不复用删掉一条就留空号否则后面做需求追溯时编号会串位。常见做法是在 Markdown 里用二级标题承载编号## FR-JOB-003 职位审核状态流转 - 前置条件职位已通过企业认证且处于待审核状态 - 流程待审核 → 通过 / 驳回驳回必须填写原因原因长度 5~200 字 - 后置条件状态变更写入审核日志含操作人、IP、时间戳 - 验收点两个审核员同时提交时只有一次生效另一次返回冲突这样写的直接好处是编号成了主键PDF 的目录层级、代码里的常量、测试用例编号可以一一对齐后面无论是做 PDF 解析还是版本比对都有稳定的锚点。2.2 用 Markdown 写 SRS再用 Pandoc 或无头浏览器打印 PDF交付物是 PDF但源文件不建议直接写 Word。源文件用 Markdown 或 HTMLPDF 只作为产物这样才能进 Git 做版本管理也能用命令一键重出。两条流水线按场景选。纯条款型、几乎不含排版要求的需求书用 Pandoc 最快# Markdown - PDF使用 xelatex 引擎以支持中文 pandoc srs.md -o srs.pdf \ --pdf-enginexelatex \ -V CJKmainfontNoto Sans CJK SC \ -V geometry:margin2.5cm \ --toc --toc-depth2 \ --number-sections \ --highlight-styletango参数逐个说明--pdf-enginexelatex是中文输出的前提pdflatex 对 CJK 支持很差-V CJKmainfont指定中文字体字体名要用fc-list :langzh查到的准确名称--toc-depth2只把一二级标题收进目录避免三级标题把目录撑成两页--number-sections自动编号但如果需求编号已经手写在标题里这个参数要去掉否则会出现1.1 FR-JOB-003这种双重编号。带表格样式、需要页眉页脚和页码的需求书走 HTML 无头浏览器打印这条路效果和web 页面 PDF 打印完全一致from playwright.sync_api import sync_playwright HEADER div stylefont-size:9px;width:100%;text-align:center公共人才招聘网 网站后台需求说明书/div FOOTER (div stylefont-size:9px;width:100%;text-align:center 第 span classpageNumber/span 页 / 共 span classtotalPages/span 页/div) with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() # 等资源加载完成保证脚本渲染的图表已经落到 DOM 上 page.goto(file:///srv/srs/srs.html, wait_untilnetworkidle) page.pdf( pathsrs.pdf, formatA4, print_backgroundTrue, # 保留表头底色和边框 margin{top: 22mm, bottom: 22mm, left: 20mm, right: 20mm}, display_header_footerTrue, header_templateHEADER, footer_templateFOOTER, ) browser.close()wait_untilnetworkidle是关键参数用load容易出现图表只渲染了一半就出 PDF 的情况print_backgroundTrue不打开表格斑马纹和状态色块会全部丢失页边距下沿要留够 22mm否则页脚页码会和正文最后一行叠在一起。2.3 中文字体与分页PDF 输出最容易翻车的 3 个参数格式问题几乎都集中在字体、断页、图片三处。这三类故障的共同点是本地预览正常、交付后才发现所以在流水线里要固定校验。参数建议值配错时的表现处理方式中文字体Noto Sans CJK SC / 思源黑体汉字变成方块或乱码字体名以fc-list :langzh输出为准容器镜像里预装字体页码与页边距上下 22mm左右 20mm页码压正文、表格被裁页脚区域在 margin 内正文不越界分页控制break-inside: avoid表格跨页断在半行对 table、pre、img 加禁止内部分页分页靠 CSS 控制比调 Pandoc 参数直接得多page { size: A4; margin: 20mm 18mm; } h2, h3 { break-after: avoid; } /* 标题不和正文分离 */ table, pre, img { break-inside: avoid; } /* 表格、代码块不跨页断开 */ tr { break-inside: avoid; } /* 单行不劈成两页 */还有一类源文件本身就是扫描件的需求书扫描版经过PDF 虚拟打印或拍照归档文字层是空的get_text()拿到的是空白。判断方法是打开后取任意页文本长度为 0 就是扫描件这类文件必须先做 OCR 或转成可编辑文本再进解析流程否则后面抽出来的条目全是空的。3. 把 PDF 需求书反向解析成可追踪的需求条目3.1 用 PyMuPDF 抽取条款编号与正文要把 PDF 用起来第一步是把它拆成编号 标题 正文 页码四元组。条款编号的形态通常是3.2.1这种点分数字用正则匹配行首即可难点在于正文和编号不在同一个文本块里需要按页内坐标顺序聚合。import re import fitz # PyMuPDF # 匹配行首的两到四级编号避免误匹配 2024 年度 这类年份 NUM_PATTERN re.compile(r^\s*(\d(?:\.\d){1,3})\s(\S.*)$) def extract_items(pdf_path: str) - list[dict]: doc fitz.open(pdf_path) items: list[dict] [] for pno in range(doc.page_count): page doc[pno] # 先判断是否有文本层扫描件直接跳过并记录 if not page.get_text().strip(): items.append({code: None, page: pno 1, flag: scanned}) continue for block in page.get_text(blocks): x0, y0, text block[0], block[1], block[4] for line in text.splitlines(): m NUM_PATTERN.match(line.strip()) if m: items.append({ code: m.group(1), title: m.group(2).strip(), page: pno 1, y: round(y0, 1), body: , }) elif items and line.strip() and items[-1].get(code): # 后续行并入当前条款正文直到遇见下一个编号 items[-1][body] line.strip() doc.close() return items逻辑说明get_text(blocks)返回带坐标的文本块y0是该块在页面中的纵坐标用于后续按阅读顺序排序items[-1][body] 这种追加方式依赖条款在文档中按顺序出现PDF 只要不是多栏排版就成立page 1是因为 PyMuPDF 页码从 0 开始而需求书里写的页码从 1 开始不修正会导致追溯时对不上页。参数上的取舍是{1,3}这个数量词允许一级到四级编号如果需求书只用到两级改成{1,2}可以进一步降低误匹配概率。3.2 需求条目入库的 SQL 表结构抽出来的条目要落库否则做成 JSON 文件用不了两周就没人维护。三张表够用文档表、条目表、追溯表。CREATE TABLE req_document ( doc_id BIGSERIAL PRIMARY KEY, project_name VARCHAR(128) NOT NULL, version VARCHAR(32) NOT NULL, file_sha256 CHAR(64) NOT NULL, -- 归档校验防止文件被替换 page_count INT NOT NULL, issued_on DATE, created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (project_name, version) ); CREATE TABLE req_item ( item_id BIGSERIAL PRIMARY KEY, doc_id BIGINT NOT NULL REFERENCES req_document(doc_id) ON DELETE CASCADE, req_code VARCHAR(32) NOT NULL, -- 如 3.2.1 或 FR-JOB-003 title VARCHAR(255) NOT NULL, body TEXT, page_no INT, kind VARCHAR(8) NOT NULL DEFAULT FR, -- FR 功能 / NFR 非功能 priority SMALLINT NOT NULL DEFAULT 2, -- 1 必须有 / 2 应该有 / 3 可以有 status VARCHAR(16) NOT NULL DEFAULT draft, UNIQUE (doc_id, req_code) ); CREATE TABLE req_trace ( trace_id BIGSERIAL PRIMARY KEY, item_id BIGINT NOT NULL REFERENCES req_item(item_id) ON DELETE CASCADE, module VARCHAR(64), -- 对应后台模块如 job-audit api_path VARCHAR(128), -- 对应接口如 /admin/jobs/audit test_case VARCHAR(128), -- 对应用例编号 updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_req_item_code ON req_item (req_code); CREATE INDEX idx_req_trace_item ON req_trace (item_id);file_sha256这一列不要省它解决的是需求书被换过一版但没人通知开发的问题比对时先算哈希不一致就直接报警。UNIQUE (doc_id, req_code)保证同一版文档里编号不重复插入冲突本身就是一种需求书有错的信号。priority用三档而不是 1~5 分是因为需求评审时把必须有和应该有区分开就够了分太细反而争论不出结果。3.3 解析结果校验编号连续性、条款覆盖率解析完直接入库风险很大正则漏掉几行、PDF 换行把编号劈成两半都会让条目数莫名少几条。入库前跑一遍校验。def check_items(items: list[dict]) - dict: codes [i[code] for i in items if i.get(code)] dup sorted({c for c in codes if codes.count(c) 1}) # 一级编号断号检查如 3 之后直接跳到 5 tops sorted({int(c.split(.)[0]) for c in codes}) holes [f{a}-{b} for a, b in zip(tops, tops[1:]) if b - a 1] return { total: len(codes), dup: dup, holes: holes, empty_body: [i[code] for i in items if i.get(code) and len(i[body]) 10], scanned_pages: [i[page] for i in items if i.get(flag) scanned], }校验项阈值失败时的处理编号重复必须为 0回到源文件核对通常是标题行被复制两份一级编号断号允许 0 处判断是真删条款还是抽取漏行反差页码定位正文为空必须为 0正文为空的条款基本是纯标题需人工补写扫描页必须为 0存在即先做 OCR否则后续追溯全断条目总数波动与上一版差 ≤ 5%波动过大说明解析规则或文档结构变了empty_body和scanned_pages这两项最容易被忽略但它们直接决定后面能不能把条款映射到接口。校验不通过就别往下走先修源文件——这一步多花的十分钟能省掉验收阶段几天的对账。4. 按需求说明书落地后台核心模块职位审核与权限4.1 RBAC 权限表设计把数据范围单独拆一列后台权限的常见错误是把菜单可见性和数据范围混在一张表里结果能点进职位列表和能看到哪些职位两条规则纠缠不清。正确做法是权限码只管能不能做这个动作数据范围单独一列。CREATE TABLE sys_role ( role_id BIGSERIAL PRIMARY KEY, role_code VARCHAR(32) NOT NULL UNIQUE, -- corp_admin / corp_hr / auditor / operator role_name VARCHAR(64) NOT NULL, data_scope VARCHAR(16) NOT NULL DEFAULT self -- all / dept / self / custom ); CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ); CREATE TABLE sys_permission ( perm_id BIGSERIAL PRIMARY KEY, perm_code VARCHAR(64) NOT NULL UNIQUE, -- job:publish / job:audit / job:offline perm_name VARCHAR(64) NOT NULL, perm_type VARCHAR(8) NOT NULL -- menu / api / button ); CREATE TABLE sys_role_permission ( role_id BIGINT NOT NULL, perm_id BIGINT NOT NULL, PRIMARY KEY (role_id, perm_id) );perm_code前后端共用一套命名前端用它控制按钮显隐后端用它做接口鉴权中间不要两套映射否则前端隐藏了按钮、后端没拦请求就是一个越权漏洞。data_scope取self时企业 HR 的职位查询必须自动追加corp_id 当前用户企业条件这条 SQL 拼接建议下沉到数据访问层统一处理而不是靠每个接口手写。4.2 职位审核状态机与接口实现需求书里的状态流转图落到代码就是一个字典。字典之外的任何跳转都判非法这样状态机永远不会被某个顺手多写一个分支的接口破坏。from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel router APIRouter() # 状态机草稿 - 待审 - 通过/驳回通过后可下线下线后允许重新提交 ALLOWED { draft: {pending}, pending: {approved, rejected}, rejected: {draft}, approved: {offline}, offline: {pending}, } class AuditIn(BaseModel): job_id: int to_status: str reason: str | None None router.post(/admin/jobs/audit) def audit_job(payload: AuditIn, userDepends(current_user), dbDepends(get_db)): # 行锁避免两个审核员同时点通过导致重复发通知 row db.query_one( SELECT status FROM job WHERE job_id %s FOR UPDATE, (payload.job_id,)) if row is None: raise HTTPException(404, 职位不存在) cur row[status] if payload.to_status not in ALLOWED.get(cur, set()): raise HTTPException(409, f状态不允许从 {cur} 变更为 {payload.to_status}) if payload.to_status rejected and not (payload.reason or ).strip(): raise HTTPException(422, 驳回必须填写原因) db.execute(UPDATE job SET status %s, updated_at now() WHERE job_id %s, (payload.to_status, payload.job_id)) db.execute( INSERT INTO job_audit_log (job_id, from_status, to_status, reason, operator_id, operator_ip) VALUES (%s, %s, %s, %s, %s, %s), (payload.job_id, cur, payload.to_status, payload.reason, user.id, user.ip)) return {job_id: payload.job_id, from: cur, to: payload.to_status}几个参数值得盯住FOR UPDATE是必须的两个审核员同时打开同一条待审职位是常态409 表示状态冲突前端拿到这个码应该刷新列表而不是弹错误框422 专门留给驳回没填原因让前端能把焦点定位到原因输入框。日志表不要只记新状态from_status和to_status都记事后查这条职位被谁从通过改回待审才有答案。4.3 参数怎么设字段校验、分页与审计日志后台字段校验规则最好写在需求书里而不是散在代码里这样验收时能逐条比对。字段规则参数值越界返回岗位名称必填长度限制4~60 字422招聘人数整数区间1~9999422薪资区间下限不大于上限单位元/月422学历要求枚举不限/大专/本科/硕士/博士422联系方式审核通过前脱敏手机号中间四位打码仅审核员可见明文列表分页单页条数上限size ≤ 100默认 20超出按 100 截断分页到深页时不要沿用OFFSET职位表几十万行以后翻到第 500 页会明显变慢改用基于job_id的游标分页首页取WHERE job_id 0 ORDER BY job_id LIMIT 20下一页把返回的最后一条job_id带回来。审计日志的保留策略也要在需求书里写清建议热表保留 6 个月、归档表长期保留归档时按created_at月分区删数据的权限只给系统管理员。5. 进阶两版需求书比对与验收对齐5.1 用 difflib 做条款级比对而不是整篇文本比对需求书出 V2 时最忌讳把两份 PDF 转成文本直接 diff输出全是噪声。按条款编号对齐后再比结果才有可读性。import difflib def diff_versions(old: list[dict], new: list[dict]) - dict: old_map {i[code]: f{i[title]} {i[body]} for i in old if i.get(code)} new_map {i[code]: f{i[title]} {i[body]} for i in new if i.get(code)} added sorted(set(new_map) - set(old_map)) removed sorted(set(old_map) - set(new_map)) changed [] for code in sorted(set(old_map) set(new_map)): if old_map[code] ! new_map[code]: # 相似度用于判断是措辞调整还是语义重写 ratio difflib.SequenceMatcher(None, old_map[code], new_map[code]).ratio() changed.append({code: code, similarity: round(ratio, 3)}) return {added: added, removed: removed, changed: changed}similarity高于 0.95 基本是标点和措辞调整改文档版本号即可0.7 到 0.95 之间通常是补充了验收条件或边界值需要回看代码有没有覆盖低于 0.7 的按新需求处理走正常的评估排期。removed列表要重点看删条款往往意味着某个已上线的功能要下线容易被忽略。5.2 从需求条目到验收用例的对照关系比对完之后把条目表和追溯表连起来跑一次覆盖率验收会上直接出这张表比口头确认高效得多。需求编号验收项判定方式FR-JOB-003并发审核只生效一次两个会话同时提交其中一个返回 409FR-JOB-005驳回未填原因被拦截空原因提交返回 422NFR-PERF-002职位列表 P95 响应10 万行数据下压测P95 小于 800msNFR-SEC-004企业 HR 越权访问用 A 企业账号请求 B 企业职位返回 403NFR-LOG-001审核日志完整性每次状态变更均有一条日志字段齐全覆盖率查询本身是一条 SQL追溯表里item_id在条目表中存在、但test_case为空的条目就是验收缺口。对相似度低于 0.9 的变更条款我会把两份原文并排贴进变更单让业务方逐字确认后再动代码。本文还有配套的精品资源点击获取
返回列表