公共人才招聘网站群架构与全文检索:J2EE/SSH与等保三级
2026/9/17 17:26:00 网站建设 项目流程

简介:企业级招聘平台建设中,J2EE 分层架构、站群与全文检索是基础概念。SSH 通过 Struts、Spring、Hibernate 解耦表现、事务与持久化,使系统适配 Oracle、MySQL 及 Tomcat、WebLogic;站群用 site_id 实现单库多站点隔离,全文检索借助 Lucene/Solr 完成中文分词、增量索引与相关性排序。其价值在于把发布审核、简历入库、跨站查询和栏目级权限沉淀为可扩展的数据模型与接口。政务招聘、公共就业服务等场景中,这些能力直接决定高并发检索、等保三级审计、单点登录与数据迁移对账是否可控。落到公共人才招聘网需求,岗位与简历表结构、跨站点检索、RBAC 和安全审计是工程重点。

1. 这份 PDF 需求说明书的真正难点不在页面

很多人拿到一份公共人才招聘网的需求说明书,第一反应是照最后那张栏目表把首页、一级栏目、二级栏目页做出来。真翻到「系统应用功能」和「安全要求」两章会发现,甲方在意的是三件事:招聘信息必须走完「发布—审核—前台显示」这条链路,求职简历要能从一个状态流转到另一个状态,「一点登录、全区查询」要真的跨站点生效。

这份需求规格说明书里 J2EE、SSH、站群、全文检索、等保三级这些词挤在一起,意味着网站后台的工作量远大于前台。它适合三类人看:接政务招聘站的技术负责人、要给遗留 J2EE 系统做二次开发的后端、以及正被跨站点检索和栏目级权限卡住的运维。

2. 从需求说明书到工程骨架:J2EE 表结构与岗位组合检索

2.1 SSH 选型的现实理由与模块切分

需求里点名 J2EE 标准、AJAX+Struts+Spring+Hibernate,同时要求兼容 Oracle、SQLServer、MySQL 与 Weblogic、Tomcat,还要提供 XML 标准接口和二次开发 Webservice 接口。这几条放在一起,说的是同一件事:业务逻辑不许绑死在某个中间件或某个数据库上。

放到今天,常见做法是 Spring Boot + MyBatis 重写。但这类项目通常要和「一卡通」就业系统做数据连接、定期向全国招聘信息公共服务网上传岗位数据,整体重写会让数据迁移和接口验收两头都说不清楚。我一般的做法是保留 SSH 的分层,把全文检索、单点登录、数据交换三块单独抽成模块,通过接口对外暴露。

# 单站点内的工程分层,站群通过 site_id 在数据层隔离 nxjob-parent/ ├── nxjob-web/ # Struts Action + JSP,只做参数校验和视图跳转 ├── nxjob-service/ # Spring 事务边界,业务规则集中在这里 ├── nxjob-dao/ # Hibernate 映射 + MyBatis 复杂查询并存 │ └── src/main/resources/mapper/JobMapper.xml ├── nxjob-search/ # 索引构建与检索,不依赖 Web 容器 ├── nxjob-sso/ # 统一用户中心客户端,给外部系统留适配层 └── nxjob-api/ # 对外 Webservice/REST 接口,供二次开发调用

nxjob-searchnxjob-sso独立成模块是有代价换来的好处:索引重建不用重启 Web 容器,检索压力大时可以把这两个模块单独部署到一台机器上,也符合需求里「数据库服务器、发布服务器、访问服务器可根据实际需求部署在不同服务器上」这条。

2.2 岗位、简历、人才库的字段设计

需求里「未入人才库简历查询」「人才库简历查询」「转入人才库」反复出现,说明简历在业务上有两种身份:投递态和入库态。如果只用一张表加 status 字段,后期按学历、专业做统计口径时一定会打架。我的做法是简历主体一张表,入库标记落在主体表上,投递关系另开一张关联表。

-- 岗位主表:站群隔离、审核流、置顶权重都在这里 CREATE TABLE t_job ( job_id NUMBER(18) PRIMARY KEY, site_id NUMBER(8) NOT NULL, -- 站群站点编号,跨站检索靠它过滤 company_id NUMBER(18) NOT NULL, -- 关联企业会员 job_name VARCHAR2(200) NOT NULL, job_category VARCHAR2(64), -- 按部颁信息分类编码存,保证与全国网一致 work_place VARCHAR2(128), degree_req VARCHAR2(32), head_count NUMBER(6) DEFAULT 1, audit_status NUMBER(2) DEFAULT 0, -- 0待审 1通过 2驳回 3关闭 weight NUMBER(6) DEFAULT 0, -- 置顶权重,越大越靠前 publish_time DATE, expire_time DATE, update_time DATE DEFAULT SYSDATE -- 增量索引用 ); CREATE INDEX idx_job_site_audit ON t_job(site_id, audit_status, publish_time);

几个字段和需求条目的对应关系值得单独列出来,改需求时按这张表回溯最快:

字段取值对应的需求条目
audit_status0/1/2/3发布—审核—显示在前台
weight整数信息权重设置与置顶排序
site_id站点编号站群管理、跨站点检索
expire_time日期过期信息关闭
update_time日期增量索引游标

简历表要多留一个保密位,因为需求明确写了人才库资料不在网站公开、只有企业会员可浏览:

CREATE TABLE t_resume ( resume_id NUMBER(18) PRIMARY KEY, member_id NUMBER(18) NOT NULL, degree VARCHAR2(32), major VARCHAR2(64), in_pool NUMBER(1) DEFAULT 0, -- 1=已转入人才库 pool_time DATE, -- 最后一次入库时间,用于人才库统计 secret_level NUMBER(1) DEFAULT 1, -- 1=仅企业会员可见 update_time DATE DEFAULT SYSDATE );

in_pool做成独立字段而不是状态枚举,是因为人才库里存在「对接成功转入资料库、需要时再转回人才库」的往复操作。用pool_time记录最后一次入库时间,统计时不会被下一次状态覆盖掉。

2.3 多字段组合检索的 SQL 与分页

需求要求职位、简历支持多字段条件组合查询,又要一般、高级、关键字、分类四种检索入口。最容易踩的坑是用字符串拼接 SQL:写得快,但参数一多就没法复用执行计划,200 并发下数据库的硬解析会直接把 CPU 拉满。我一般把条件写成动态 SQL,让数据库缓存执行计划。

<select id="searchJobs" parameterType="map" resultMap="JobMap"> SELECT job_id, job_name, company_id, work_place, publish_time FROM t_job <where> audit_status = 1 AND expire_time &gt; SYSDATE <if test="siteIds != null and siteIds.size() &gt; 0"> AND site_id IN <foreach collection="siteIds" item="sid" open="(" close=")" separator=","> #{sid} </foreach> </if> <if test="category != null"> AND job_category = #{category} </if> <if test="place != null"> AND work_place LIKE #{place} || '%' </if> <if test="degree != null"> AND degree_req = #{degree} </if> <if test="keyword != null"> AND job_name LIKE '%' || #{keyword} || '%' </if> </where> ORDER BY weight DESC, publish_time DESC </select>

参数说明:siteIds对应站群多站点,主站查全站就把所有站点编号放进去,市县区子站只放自己的编号;keyword的 LIKE 只是兜底,真正的关键字检索交给第 3 章的索引模块,因为LIKE '%x%'走不了索引,大表上必然超时。

分页在 Oracle 里不要直接套rownum两层嵌套,ORDER BY会被压进内层导致排序整个结果集。常见做法是先取主键分页、再用主键回表查明细,代价是多一次查询,收益是翻到第 100 页时响应时间不塌。

还有一点容易忽略:简历检索的 SQL 里不要靠一个 flag 控制可见性。人才库资料只对企业会员开放,这个判断应该在 Service 层根据当前登录身份决定要不要拼secret_level = 0条件,写进 SQL 模板反而容易漏。

3. 站群架构与跨站点全文检索落地

3.1 一套库多站点:站群不是复制多套系统

需求里「站群管理完全通过浏览器实现远程管理」「支持站点克隆」「支持服务器注册」这几条,说的是一个后台管多个站点。很多人第一反应是每个市县区部署一套,那样数据共享和全区统计就废了。我的做法是单库多站点:内容表全部带site_id,栏目表带site_idparent_id,站点表存域名和模板目录。

CREATE TABLE t_site ( site_id NUMBER(8) PRIMARY KEY, site_code VARCHAR2(32) NOT NULL, -- 如主站代码、市县区代码,用于子域名映射 domain VARCHAR2(128), -- xxx.nxjob.gov.cn template_id NUMBER(8), -- 站点克隆时复制这一行 status NUMBER(1) DEFAULT 1 );

站点克隆的本质是把模板目录复制一份、把栏目树按site_id复制一份,而不是复制数据库实例。栏目树的复制用一条INSERT ... SELECT配合序列就能完成,克隆出的站点默认全部内容都是草稿态,避免克隆完直接对外可见。

发布环节,需求要求发布服务器同时向多个 Web 访问服务器发布信息。常见做法是发布服务器生成静态页到本地目录,再用 rsync 推送到各访问服务器,脚本里加--delete保证下架的文章同步删掉。

#!/bin/bash # 静态页发布:访问服务器清单在 sites.conf 里,每行一个主机名 PUBLISH_DIR=/data/publish LOG=/var/log/nxjob/publish_$(date +%Y%m%d).log for host in $(cat /etc/nxjob/sites.conf); do rsync -az --delete -e "ssh -o BatchMode=yes" \ "$PUBLISH_DIR/" "$host:/data/www/nxjob/" >>"$LOG" 2>&1 \ || echo "publish failed: $host" >>"$LOG" done

参数说明:-a保留权限与时间戳,-z传输压缩,--delete删除目标端已不存在的文件(对应过期信息关闭后静态页要下架),BatchMode=yes避免交互提示卡住定时任务。注意这个脚本默认信任主机密钥,第一次部署要先手工执行一次建立 known_hosts,否则凌晨的定时发布全部静默失败,日志里只剩一行非零退出码。

提示:静态页只生成栏目列表页和内容页,搜索结果页和会员中心必须走动态,否则权限判断会失效,企业会员一退出仍能从静态页看到缓存内容。

3.2 中文分词与增量索引

需求写得很具体:中文自动分词、中文整词与普通字符串同时支持、非精确匹配、相关性排序、增量索引、支持 Word/TXT/PDF 检索。这一组能力基本只能靠 Lucene/Solr 这一层实现,数据库的 LIKE 做不到。

增量索引的关键是有一个可靠的时间游标。表里已经有update_time,定时任务每次只取上次索引完成时间之后的记录:

// 增量索引:时间游标持久化在文件里,避免每次全量扫表 Date last = IndexCursor.read(); // 上次索引成功完成的时间 String sql = "select job_id, job_name, job_desc, update_time from t_job " + "where update_time > ? order by update_time"; // 每 500 条提交一次:控制堆内存,同时避免大批量失败后整批重做 // 索引 commit 成功之后才写回游标,顺序反了会永久丢数据

游标一定要在索引提交成功之后才写回。先写游标再建索引,中途失败就会永久漏掉那一批数据,这是增量索引最常见的丢数据原因。另外 Oracle 的update_time精度只到秒,同一秒内多次更新可能被跳过,稳妥做法是把游标回退 1 到 2 秒再查,用主键幂等覆盖掉重复数据。

分词词典需要单独维护。把当地产业相关的专有名词(行业名、园区名、岗位俗称)加进自定义词典,否则会被切成单字,检索命中率会明显下降。

3.3 二次检索与相关性排序

需求把检索能力逐项列了出来,包括模糊查询、布尔表达式、万用字符、多次递进查询、指定外部网站检索、跨站点检索和结果二次查询。映射到实现手段大致是这样:

需求项实现手段
模糊查询分词后的多词 OR 匹配,不做前缀通配
布尔表达式查询解析器支持 AND/OR/NOT 语法
二次递进查询缓存上次结果的 docId 集合,下次加 filter
跨站点检索索引合并,查询时按 site_id 过滤
指定外部网站检索单独建外部源索引,不混进主索引
非精确匹配排序按相关度打分,权重字段作为次级排序

二次检索的实现要点是不要重查全库再取交集。把上一次的 docId 集合放在会话里,下一次查询直接当过滤条件用:

// 二次检索:限定在上一次结果集内,避免全库重扫 Integer[] lastIds = (Integer[]) session.getAttribute("lastJobIds"); BooleanQuery.Builder q = new BooleanQuery.Builder(); q.add(new TermQuery(new Term("site_id", siteId)), Occur.FILTER); if (lastIds != null && lastIds.length > 0) { // 具体 API 随 Lucene 版本略有差异,思路是加一个 id 白名单过滤器 q.add(IdsFilter.of(lastIds), Occur.FILTER); } q.add(parser.parse(keyword), Occur.MUST);

相关度排序上有个坑:置顶权重字段如果直接参与打分,会把相关度高的结果压下去。正确做法是权重只做次级排序键,先按相关度排,相关度相同再按权重和发布时间排。

4. 统一用户中心、栏目级 RBAC 与发稿量统计

4.1 用户-角色-权限-栏目四张表

需求对权限的描述很细:组权限与角色权限单独设置、权限可细化至栏目、超级管理员可继续向下分配、按业务流程或栏目或单位组织结构分配。这种需求用「用户表加一个 role 字段」撑不住,落到模型上是四张表加一张关联表。

CREATE TABLE t_role ( role_id NUMBER(8) PRIMARY KEY, role_name VARCHAR2(64), site_id NUMBER(8) -- 角色归属站点,不同站点可各自维护人员 ); CREATE TABLE t_permission ( perm_id NUMBER(10) PRIMARY KEY, perm_code VARCHAR2(64), -- 如 job:audit、article:publish、resume:search perm_name VARCHAR2(64) ); CREATE TABLE t_role_perm ( role_id NUMBER(8), perm_id NUMBER(10), column_id NUMBER(10), -- 为空表示全栏目,非空表示仅该栏目 PRIMARY KEY (role_id, perm_id, column_id) );

column_id放进关联表而不是写死在权限表里,才能实现「同一个编辑在 A 栏目能发稿、在 B 栏目只能看」。多一个字段,省掉了后期按站点、按单位组织结构反复加表的麻烦。权限场景和落点的对应关系:

权限场景数据落点
按栏目发稿t_role_perm.column_id
按站点隔离管理t_role.site_id
跨站只读同一 user 挂多个站点的只读角色
超级管理员向下授权新增 t_role 并绑定部分 perm_code

4.2 单点登录与登录超时

需求里「各种应用系统间跨域的单点登录、退出和统一的用户信息管理」指向统一用户中心。常见做法是票据式认证,业务系统不处理密码,只认票据。同时需求明确要求登录超时判断,超过指定时间不操作,再次操作必须重新登录。

<!-- 会话超时:需求要求超时后再次操作必须重新登录 --> <session-config> <session-timeout>30</session-timeout> </session-config> <!-- 单点登录客户端过滤器:未认证请求一律跳转到统一认证中心 --> <filter> <filter-name>authFilter</filter-name> <filter-class>com.nxjob.sso.client.AuthFilter</filter-class> <init-param> <param-name>loginUrl</param-name> <param-value>https://sso.example.gov.cn/login</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>https://www.example.gov.cn</param-value> </init-param> </filter>

参数说明:session-timeout单位是分钟,后台管理系统 30 分钟是常见取值;serverName必须填实际对外域名,站群下各子站点如果都填主域名,认证跳转回来会串站,用户会看到别人的会话。外部就业系统的对接不要指望对方按同一套协议,通常是在自己这一侧写适配类,把对方的身份标识映射成本地会员编号。

4.3 发稿量统计与绩效考核

需求要求看到每个管理员添加了多少内容、加在哪些栏目、什么日期。这就是一条分组统计,不需要额外埋点,数据本来就在内容表里。

-- 按管理员统计发稿量,时间范围由前端传入 SELECT u.user_name, c.column_name, COUNT(*) AS article_cnt, MIN(a.create_time) AS first_publish, MAX(a.create_time) AS last_publish FROM t_article a JOIN t_user u ON u.user_id = a.create_user JOIN t_column c ON c.column_id = a.column_id WHERE a.create_time BETWEEN TO_DATE(#{begin}, 'yyyy-mm-dd') AND TO_DATE(#{end}, 'yyyy-mm-dd') + 1 GROUP BY u.user_name, c.column_name ORDER BY article_cnt DESC;

+1是把结束日期补到当天 23:59:59,否则按日期传参会漏掉最后一天的数据,这是后台统计最常被业务方投诉的一处。ORDER BY直接对应需求里的「按发稿量多少排序」。数据量大的时候,这个查询要改成先按月份汇总成中间表,不然每次打开绩效考核页都要全表扫一遍。

5. 等保三级下的 IP 规则、审计与迁移对账

需求要求在后台添加 IP 规则、设定地址段范围来控制系统访问,同时要求完整的审计日志记录所有登录与操作。这两件事都不适合写死在反向代理配置里,规则一变就要重启服务。常见做法是把 IP 段存表,在应用层过滤器判断,变更即时生效。

CREATE TABLE t_ip_rule ( rule_id NUMBER(10) PRIMARY KEY, ip_start VARCHAR2(64), ip_end VARCHAR2(64), rule_type NUMBER(1), -- 1 白名单 2 黑名单 site_id NUMBER(8), memo VARCHAR2(200) );

审计日志单独建表并按月分区,登录、审核、删除、导出四类操作必须落库,记录操作人、IP、目标主键和结果。别把审计日志和业务日志混在一张表里,日志清理策略不一样,混在一起后想保留一年审计记录就得把业务日志也留着。

备份只做不演练等于没做。我是每周用最近一个备份文件在测试实例做一次完整还原,确认能起到需求里写的「一键还原到之前的状态」,而不是等到真出事才发现备份文件损坏。

数据迁移最容易出问题的是顺序和编码。需求要求检索接口支持多种内码,说明老库里的编码本身就不统一,导入前先抽样检查比事后改数据便宜得多:

# 抽样检查源库导出的文本是否混用编码,再决定统一转成 UTF-8 file -i dump/resume_sample.txt iconv -f GBK -t UTF-8 dump/resume_sample.txt > /tmp/check.txt 2>>/tmp/iconv.err wc -l /tmp/iconv.err # 错误行数不为 0 说明样本里存在非 GBK 内容

迁移顺序按依赖走:先字典表(行政区划、专业、工作地点),再企业会员,再岗位,最后简历和附件。每张表跑完立刻做条数对账,而不是全部导完统一验证——出了问题能马上定位到哪张表,回滚成本也从「重来一遍」降到「重跑一张表」。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询