微信小程序求职招聘系统:SSM到SpringBoot与投递状态机
简介:面向高校课程设计、毕业设计及小程序全栈练手场景的微信小程序求职招聘系统源码包,采用微信小程序客户端、Java后台与MySQL数据库,后端基于SSM并可升级至SpringBoot。系统划分求职者、商家、管理员三类角色:求职者和商家在小程序端完成微信授权登录、个人资料完善、兼职分类浏览、岗位联系、面试预约;商家经学校认证后可发布兼职、处理面试请求,并配合互评评分与客服系统形成闭环,管理员在Web端后台维护数据。压缩包共1222个文件、约4.09MB,涵盖html、js、css等Web页面与脚本,java、jsp、sql等后端与数据库文件,以及wxml、wxss、json等小程序页面结构和配置文件,另有png、gif等界面素材,便于对照前后端实现与部署。已有312人学习,适合需要完整业务闭环、数据库脚本和可扩展后端架构的读者参考二次开发。
1. 从一份“能跑起来”的求职小程序说起:需求边界与选型判断
做微信小程序求职招聘系统的第一步不是画页面,而是先想清楚一件事:这个系统的核心是把“投递之后的状态”追踪清楚,而不仅仅是“发布职位”。很多人上来就把小程序首页做成职位瀑布流,结果企业端连“谁投了我、投了几次、简历看到哪一步”都查不到,答辩时被追问一句就卡住。这套方案要解决的是求职者、企业HR、平台管理员三端在同一个岗位池上协作的问题:后端用 SSM 打底、预留 SpringBoot 升级路线,小程序端只做轻量渲染和状态展示。它适合正在做毕设或课程设计、需要一套能跑通、也能讲清分层结构的人,也适合想拿现成后端骨架改造成招聘类业务的开发者。
2. 需求拆解与角色建模:三端职责、功能矩阵与投递状态机
招聘类系统和普通信息发布系统的分水岭,就在“角色”这两个字。同一个职位,求职者看到的是薪资、城市、学历门槛和“投递”按钮;企业HR看到的是投递人数、简历附件和“邀面试”按钮;管理员看到的是企业资质审核队列和违规岗位下架入口。如果一开始不把这三端的权限边界画出来,后面所有接口都会写成“谁都能调”,最后不得不在 Controller 里堆一堆 if 判断,改一次崩一次。所以方案落地的顺序是:先出功能矩阵,再定投递状态机,最后补非功能性约束。
2.1 三类角色的功能矩阵与权限边界
我把三端的职责拆成一张表,每个功能点都对应一个后端接口前缀,这样后面写 Controller 时不会互相污染。这张表的用法很直接:做毕设的话,它就是你需求分析章节的骨架;做交付的话,它就是你和需求方对齐范围的依据。
| 角色 | 核心功能 | 接口前缀 | 数据可见范围 |
|---|---|---|---|
| 求职者 | 登录、简历维护、职位检索、投递、查看进度、撤回投递 | /api/seeker | 仅自己的简历与投递记录 |
| 企业HR | 企业认证、职位发布/上下架、简历筛选、发起面试、录用/拒绝 | /api/hr | 仅本企业职位与投递到本企业的简历 |
| 平台管理员 | 企业资质审核、职位审核、举报处理、数据统计 | /api/admin | 全量,但简历正文脱敏 |
这张表背后有一条硬规则:任何一端的接口都不能靠前端传 enterpriseId 或 seekerId 来限定范围,必须从登录态里取。小程序端的参数是用户能随手改的,前端传什么后端都信,等于没做权限。通常的做法是登录时把角色和主体 ID 一起写进 Redis 的登录态,拦截器解析 token 后放进 ThreadLocal,Service 层只认 ThreadLocal 里的值。
另一条容易忽略的是企业认证状态。企业注册后是“待审核”,这时候可以发职位草稿,但职位不能进求职者的搜索结果。把这个状态位做进职位表而不是只放企业表,是为了查询时少一次 join 判断——虽然我们后面还是 join 了企业表,但状态字段冗余一份能让你在只查职位表做统计时少写一堆关联。
2.2 投递流程的状态机设计
投递是整个系统里唯一有“生命周期”的实体,也是最容易写乱的地方。如果只用一个 status 字段随便 update,很快就会出现“已拒绝的又被改成录用”“求职者自己把状态改成已录用”这种事故。我的做法是把合法流转关系写死成一张表:
- 0 已投递 → 1 已查看 / 5 已撤回(求职者操作)
- 1 已查看 → 2 邀面试 / 4 已拒绝(HR 操作)
- 2 邀面试 → 3 已录用 / 4 已拒绝(HR 操作)
- 3、4、5 为终态,不再流转
这张表的价值在于,所有状态变更走同一个transfer方法,非法流转直接抛业务异常。答辩时被问“如果 HR 误操作把候选人拒了怎么办”,你可以答:终态不可逆,但保留了重新发起投递的入口,且同岗位同求职者只允许一条有效投递记录(靠唯一索引兜底)。这个回答比“加个撤销按钮”专业得多。
状态机还要考虑并发。HR 在后台点“邀面试”的同时,求职者在小程序点“撤回”,两个请求几乎同时到达。如果只是先 select 再 update,后提交的那个会覆盖前一个。后面第 4 章会给带版本号条件更新的写法,这里先记住结论:状态流转必须做条件更新,update ... where id = ? and status = ?,影响行数为 0 就说明被别人改过了。
2.3 非功能性需求:审核、时效与数据留存
招聘系统有三个躲不掉的非功能点,写方案时最好显式列出来,否则验收阶段会被追问。
第一是审核链路。企业资质、职位内容都要过一遍人工审核,审核不通过的职位不能出现在列表里,但数据要保留,方便企业修改后重新提交。实现上就是职位表一个 status,审核记录单独一张表存操作人和时间,别把审核意见塞进职位表的备注字段,后面查历史根本查不出来。
第二是职位时效。岗位发布 30 天自动过期,过期后状态置为 3,不再进列表,但求职者的历史投递记录里还能看到这个岗位的快照信息。用一个@Scheduled每天凌晨扫一遍即可,量小的时候不需要引入消息队列。这里有个细节:过期任务要按publish_time建索引,否则全表扫描在数据量上来后会把数据库拖慢。
第三是简历数据留存。简历附件一般存对象存储或者本地磁盘,数据库只存 URL 和文件指纹。求职者删除简历时,数据库做逻辑删除,文件保留 90 天再物理清理,避免企业已经下载的附件突然 404。这类约定写进方案里,会让整份设计看起来是有人真正维护过的。
3. 总体架构设计:SSM 打底、SpringBoot 平滑接管的双层结构
标题里“SSM 可升级 SpringBoot”不是一句噱头,它决定了整个后端的组织方式。SSM 阶段用 web.xml 装配 DispatcherServlet、用 spring-*.xml 声明 Bean、用 MyBatis 的 SqlSessionFactoryBean 手动配置;升级到 SpringBoot 之后,这些全部交给 starter 和自动配置,业务代码一行不动。要做到“一行不动”,关键是在 SSM 阶段就按 SpringBoot 的包结构写代码,别把配置和业务搅在一起。
3.1 分层结构与小程序端的交互链路
后端分四层:Controller 只做参数校验和响应包装,Service 承载业务规则和事务边界,Mapper 只做 SQL,DTO/VO 负责内外隔离。实体类不直接返回给小程序,原因是数据库字段一变前端就崩,而且会把password、openid这类不该露的字段带出去。VO 多写一个类,换来的是接口稳定。
小程序端到后端的链路是:wx.login拿 code → 调/api/auth/login→ 后端用 code 换 openid 并签发 token → 小程序把 token 存 Storage → 后续请求统一带Authorization头。整条链路里,session_key和secret必须留在服务端,一旦下发到小程序包体里,反编译就能拿到,这是硬性红线。
小程序页面 │ wx.request(统一封装,带 token) ▼ Nginx(HTTPS 终止、静态资源、限流) ▼ Spring MVC DispatcherServlet ▼ AuthInterceptor(解析 token → ThreadLocal 上下文) ▼ Controller → Service(事务) → Mapper → MySQL │ └── Redis(登录态、热门职位缓存)上面这条链路里,拦截器是必须的,但要注意放行白名单:登录接口、职位列表接口(未登录也能浏览)、静态资源。白名单写成配置文件里的数组,别硬编码在拦截器里,升级 SpringBoot 后可以直接挪到 yml。
分层的另一个好处是可测试。Service 层不依赖HttpServletRequest,单元测试可以直接 new 出来跑,Mock 掉 Mapper 就行。很多课设的通病是所有逻辑写在 Controller 里,一测就得启动整个容器,最后干脆不测。
3.2 从 SSM 到 SpringBoot 的升级路径
升级这件事,最怕的是“重写一遍”。正确姿势是先让 SSM 版本跑通,再按下面四步换装配方式,业务代码零改动。
# 第 1 步:pom 依赖替换,把 spring-webmvc、mybatis、druid 换成 starter 版本 # spring-boot-starter-web / mybatis-spring-boot-starter / druid-spring-boot-starter # 第 2 步:新增启动类,放在原 com.demo 包的根目录下,自动扫描原有 Controller/Service # 第 3 步:web.xml 删除,DispatcherServlet 由容器自动注册 # 第 4 步:spring-dao.xml / spring-service.xml 里的 dataSource、事务管理器、MapperScannerConfigurer 全部删除 # 改由 application.yml + 自动配置接管,SqlSessionFactory 不再手写 mvn -q clean package -DskipTests java -jar target/job-app.jar --spring.profiles.active=dev配置迁移是最容易出错的一步,给一张对照表,照着替换基本不会翻车:
| SSM 阶段 | SpringBoot 阶段 | 注意点 |
|---|---|---|
jdbc.properties | application.yml的spring.datasource | 密码走环境变量,别提交到仓库 |
spring-dao.xml里的MapperScannerConfigurer | @MapperScan("com.demo.mapper") | 加在启动类上,只扫 Mapper 包 |
spring-service.xml的DataSourceTransactionManager | 自动配置提供 | 自定义事务管理器要显式声明@Bean |
web.xml的CharacterEncodingFilter | server.servlet.encoding | 默认就是 UTF-8 |
手动SqlSessionFactoryBean | mybatis.mapper-locations | xml 路径用classpath:mapper/*.xml |
升级后还会多出两个白送的能力:一是@SpringBootTest直接起容器做集成测试;二是actuator的健康检查端点,答辩演示时打开/actuator/health显示UP,比嘴上说“服务正常”有说服力。但 actuator 端点记得加权限或改端口,别和生产接口混在一起暴露。
3.3 登录态与鉴权方案选型
小程序端的登录态有两条常见路线:JWT 和 Redis Token。JWT 无状态、不用查库,但没法主动失效;Redis Token 每次请求查一次缓存,但能踢人、能续期、能存角色。
这个项目里我选 Redis Token,原因是招聘系统有“企业被冻结立即生效”的需求,JWT 签发后到过期前都拦不住,只能再加一层黑名单,反而更复杂。Redis 方案下,token 就是随机串,值里存用户ID:角色,过期时间 7 天,每次请求命中后把 TTL 续到 7 天,实现“活跃用户不掉线”。
// 拦截器核心逻辑:解析 token → 续期 → 写入上下文 String token = request.getHeader("Authorization"); String val = redis.opsForValue().get("login:" + token); if (val == null) { response.setStatus(401); return false; // 前端收到 401 后静默重登一次 } String[] parts = val.split(":"); // 格式 userId:role AuthContext.set(Long.valueOf(parts[0]), parts[1]); redis.expire("login:" + token, 7, TimeUnit.DAYS); // 滑动续期 return true;参数说明:AuthContext用 ThreadLocal 实现,请求结束必须在afterCompletion里remove(),否则线程池复用会串号——这是 Tomcat 下最典型的“用户 A 看到用户 B 数据”事故来源。角色只放一个字段是因为本系统里一个账号只属于一个角色,企业 HR 和求职者不共用账号;如果业务要求一人多角色,这里要存成逗号分隔的集合,拦截器判断时改成contains。
4. 数据与核心模块落地:六张主表撑起投递闭环
表设计不用贪多,招聘系统的骨架就是六张:用户、企业、职位、简历、投递、消息。其余如面试记录、举报、审核日志,都是围绕这六张挂上去的。下面按“建表 → 登录 → 检索 → 状态流转”的顺序落地,每一段都能直接抄进项目里改。
4.1 核心表结构与索引策略
先看职位表和投递表,这两张表决定了系统的查询性能和数据一致性。
-- 职位表:列表页的过滤条件全部落在一条联合索引上 CREATE TABLE `job` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `enterprise_id` BIGINT NOT NULL COMMENT '所属企业', `title` VARCHAR(64) NOT NULL COMMENT '职位名称', `city` VARCHAR(32) NOT NULL COMMENT '工作城市', `salary_min` INT NOT NULL COMMENT '薪资下限,单位千元', `salary_max` INT NOT NULL COMMENT '薪资上限,单位千元', `education` TINYINT NOT NULL DEFAULT 0 COMMENT '0不限 1大专 2本科 3硕士', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0草稿 1招聘中 2已下架 3已过期', `publish_time` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_city_status_time` (`city`, `status`, `publish_time`), KEY `idx_ent` (`enterprise_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 投递表:唯一索引是防重复投递的最后一道闸 CREATE TABLE `delivery` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `job_id` BIGINT NOT NULL, `seeker_id` BIGINT NOT NULL, `resume_id` BIGINT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0已投递 1已查看 2邀面试 3已录用 4已拒绝 5已撤回', `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_job_seeker` (`job_id`, `seeker_id`), KEY `idx_seeker_status` (`seeker_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;两个设计点值得讲清。第一,idx_city_status_time的字段顺序不是随便排的:列表页默认按城市筛、只查招聘中、按发布时间倒序,把等值条件放前面、范围排序放最后,才能让ORDER BY publish_time DESC走上索引而不触发 filesort。如果你的检索还带薪资下限,可以再加一个(city, status, salary_max)的索引,但别超过三个联合索引,写入会变慢。
第二,uk_job_seeker唯一索引承担了“同一岗位不能重复投递”的职责。业务层也会先查一次给出友好提示,但真正兜底的是索引,并发下两个请求同时通过业务校验时,第二个会抛DuplicateKeyException,捕获后转成“你已经投过这个岗位了”。撤回后再投的场景,用status = 5的记录更新回 0,而不是插入新行,避免历史记录分裂。
4.2 微信登录换 token 的实现
登录接口是小程序端唯一不需要 token 的接口,也是最需要小心的地方。
@RestController @RequestMapping("/api/auth") public class WxAuthController { @Value("${wx.appid}") private String appid; @Value("${wx.secret}") private String secret; // 只从配置读,绝不写进小程序 @Autowired private RestTemplate restTemplate; // SpringBoot 下由容器注入 @Autowired private StringRedisTemplate redis; @Autowired private UserMapper userMapper; @PostMapping("/login") public Result<String> login(@RequestBody LoginDTO dto) { // 1. code 换 openid:code 一次性有效,有效期 5 分钟 String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; JSONObject session = restTemplate.getForObject(url, JSONObject.class); String openid = session == null ? null : session.getString("openid"); if (openid == null) { return Result.fail(401, "登录凭证已失效,请重试"); } // 2. 首次登录自动建号,角色默认求职者,HR 走企业认证流程升级 User user = userMapper.selectByOpenid(openid); if (user == null) { user = User.newSeeker(openid, dto.getNickname()); userMapper.insert(user); } // 3. 签发 token 写 Redis,值为 userId:role String token = UUID.randomUUID().toString().replace("-", ""); redis.opsForValue().set("login:" + token, user.getId() + ":" + user.getRole(), 7, TimeUnit.DAYS); return Result.ok(token); } }code只能用一次、5 分钟过期,这是 wx.login 的固有约束,所以小程序端每次调登录都要重新wx.login,不能缓存 code 复用。session_key拿到后只在服务端做解密用(比如获取手机号),不要返回前端。RestTemplate在 SSM 阶段是手动 new 的,升级 SpringBoot 后改成注入,这也是“可升级”设计的一个具体体现——类里的写法不变,只是装配方式变了。
小程序端的请求封装决定了后面几十个页面写起来累不累:
// utils/request.js —— 统一带 token、统一错误提示、401 静默重登 const BASE_URL = 'https://api.example.com'; // 换成自己的已备案域名 function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 401 && !options._retried) { // 只重放一次,避免 token 一直失效导致死循环 reLogin().then(() => request(Object.assign({}, options, { _retried: true }))) .then(resolve).catch(reject); return; } if (res.data && res.data.code === 0) resolve(res.data.data); else { wx.showToast({ title: (res.data && res.data.msg) || '请求失败', icon: 'none' }); reject(res.data); } }, fail: reject }); }); }_retried这个标记是必须的。没有它,token 失效 + 重登失败会形成递归调用,小程序直接卡死,这是玄学问题里最常见的一种——表面上“偶尔白屏”,实际是请求风暴。reLogin内部就是再调一次wx.login拿新 code 换 token 写 Storage,代码很短但一定要包 try/catch,失败就跳登录页。
4.3 职位检索与分页查询
检索接口的坑主要在两个地方:分页插件的位置和 SQL 里条件的拼装顺序。
@Service public class JobQueryService { @Autowired private JobMapper jobMapper; public PageInfo<JobVO> search(JobQuery q) { // PageHelper 必须紧贴查询语句,中间不能插别的 SQL,否则分页会作用到错误的语句上 PageHelper.startPage(q.getPageNum(), q.getPageSize()); List<JobVO> list = jobMapper.searchByCondition( q.getKeyword(), q.getCity(), q.getSalaryFloor(), q.getEducation()); return new PageInfo<>(list); } }<select id="searchByCondition" resultType="com.demo.vo.JobVO"> SELECT j.id, j.title, j.city, j.salary_min, j.salary_max, j.education, e.name AS enterprise_name FROM job j JOIN enterprise e ON e.id = j.enterprise_id WHERE j.status = 1 AND e.verify_status = 1 <!-- 未过审企业的岗位不进列表 --> <if test="city != null and city != ''">AND j.city = #{city}</if> <if test="salaryFloor != null">AND j.salary_max >= #{salaryFloor}</if> <if test="education != null">AND j.education <= #{education}</if> <if test="keyword != null and keyword != ''"> AND (j.title LIKE CONCAT('%', #{keyword}, '%') OR e.name LIKE CONCAT('%', #{keyword}, '%')) </if> ORDER BY j.publish_time DESC </select>参数说明:salaryFloor是求职者期望的薪资下限,用salary_max >= salaryFloor匹配,语义是“岗位上限够得着我的期望”,比salary_min >=更符合真实筛选习惯。education用<=是因为学历门槛是上限约束,本科岗位大专生看不到,但硕士能看到本科岗位。
LIKE '%关键词%'前置通配符走不了索引,这是明摆着的性能债。数据量小的时候无所谓,超过十万条就要换成全文索引或者把关键词拆词后走倒排。方案里我会写清楚这条边界,答辩被问“数据量大了怎么办”,答“当前方案适用万级数据,十万级以上引入 ES 或 MySQL 全文索引”,比硬说性能很好可信。
4.4 投递状态流转的落地
状态机落地就两个要点:合法流转校验和条件更新。
private static final Map<Integer, Set<Integer>> FLOW = new HashMap<>(); static { FLOW.put(0, new HashSet<>(Arrays.asList(1, 5))); // 已投递 -> 已查看 / 已撤回 FLOW.put(1, new HashSet<>(Arrays.asList(2, 4))); // 已查看 -> 邀面试 / 已拒绝 FLOW.put(2, new HashSet<>(Arrays.asList(3, 4))); // 邀面试 -> 已录用 / 已拒绝 FLOW.put(3, Collections.emptySet()); // 终态 FLOW.put(4, Collections.emptySet()); FLOW.put(5, Collections.emptySet()); } @Transactional(rollbackFor = Exception.class) public void transfer(Long deliveryId, int target, String role) { Delivery d = deliveryMapper.selectById(deliveryId); if (d == null) throw new BizException("投递记录不存在"); if (!FLOW.getOrDefault(d.getStatus(), Collections.emptySet()).contains(target)) { throw new BizException("非法状态流转:" + d.getStatus() + " -> " + target); } if ("SEEKER".equals(role) && target != 5) { throw new BizException("求职者只能撤回投递"); } // 条件更新:status 作为乐观锁条件,影响行数为 0 说明并发冲突 int rows = deliveryMapper.updateStatus(deliveryId, d.getStatus(), target); if (rows == 0) throw new BizException("状态已被更新,请刷新后重试"); messageService.pushAsync(d.getSeekerId(), target); // 站内信异步推,不阻塞主流程 }<update id="updateStatus"> UPDATE delivery SET status = #{target} WHERE id = #{id} AND status = #{expect} </update>updateStatus的status = #{expect}就是乐观锁,不需要额外加 version 字段,因为状态本身就是版本。这比select ... for update轻,缺点是冲突时要让用户重试,但招聘场景的并发冲突概率极低,完全够用。pushAsync用@Async或简单的线程池提交,别在事务里同步发消息——消息服务一慢,整个投递接口就超时。
5. 避坑与常见问题排查:五个最容易翻车的点
这套结构跑通不难,难的是跑稳。下面五条是同类项目里出现频率最高的问题,每条都按“现象 → 原因 → 解决”写,出问题时可以当排查清单用。
5.1 登录态类问题
现象一:用户反馈“明明登录了,过一会儿又让我重新登录”。
原因通常是 token 的续期逻辑写在了登录接口里,而不是拦截器里,导致只有主动调登录才会续期;另一种是 Redis 的 key 设了固定过期时间但没做滑动续期。还有一种隐蔽情况:小程序切后台超过一定时间,Storage 还在但 Redis 里的 key 已经过期。
解决是把续期放进拦截器命中之后,用expire重置 TTL;前端在收到 401 时先静默重登一次,成功就重放原请求,失败才跳登录页。同时给 token 加一个“最后活跃时间”字段,超过 30 天未活跃直接失效,避免长期不用还留着有效凭证。
现象二:两个用户看到对方的数据。
原因几乎都是 ThreadLocal 没清理。线程池复用线程,上一个请求的AuthContext残留下来,下一个请求如果没解析出 token(比如走了白名单分支),就会读到脏值。
解决是在拦截器的afterCompletion里无条件remove(),并且AuthContext.get()在取不到值时直接抛异常而不是返回 null,让问题在开发阶段就暴露出来,而不是等线上偶发。
5.2 数据与并发类问题
现象三:同一个岗位出现两条投递记录。
原因是业务层“先查再插”没有原子性,两个请求同时通过查询校验后都执行了插入。只靠业务校验是挡不住的。
解决是给(job_id, seeker_id)加唯一索引,插入时捕获DuplicateKeyException转成业务提示;撤回后的重新投递走更新而非插入。这条规则在任何“防重复”场景都通用:数据库约束是最后一道闸,业务校验只是为了让提示更友好。
现象四:列表页越翻越慢,最后一页卡十几秒。
原因通常是ORDER BY字段没有索引,或者分页参数被前端传成了负数、超大值。还有一种高频原因是 PageHelper 的startPage和查询语句之间隔了别的查询,导致分页作用到了错误的 SQL 上,返回的数据量异常。
解决是给排序字段建联合索引,Service 入口对pageNum、pageSize做上下限校验(pageSize最大 50),并检查startPage是否紧贴查询。翻到深页仍然慢的话,改成基于publish_time的游标分页,别死磕 offset。
5.3 升级与部署类问题
现象五:本地跑得好好的,打成 jar 之后所有 Mapper 都找不到。
原因是 MyBatis 的 xml 放在src/main/java目录下,Maven 默认不会把 java 目录里的非 class 文件打进包,SSM 阶段用 IDE 直接跑所以没暴露。
解决是把 xml 统一放到src/main/resources/mapper/,并在配置里写mybatis.mapper-locations=classpath:mapper/*.xml;如果坚持放在 java 目录,就得在 pom 里配resources的 include。同类问题还有application.yml放在错误位置导致配置不生效,现象是“连的库还是本地库”,排查时先看启动日志里打印的 profiles 和数据源 URL,别靠猜。
6. 验收与答辩:怎么证明这套系统真的能用
验收这一步,很多人栽在“功能都点了,但说不清边界”。我的建议是准备两张表,一张功能验收清单,一张接口用例表,现场演示前先自己走一遍。
| 验收项 | 判定标准 | 演示方式 |
|---|---|---|
| 登录与身份 | openid 唯一,重复登录不建重复账号 | 两个小程序账号分别登录,后台查 user 表 |
| 权限隔离 | HR 调求职者接口返回 403 | 用 HR token 请求/api/seeker/resume |
| 检索准确性 | 城市 + 学历 + 薪资三条件组合结果正确 | 三种条件组合各查一次,比对总数 |
| 投递闭环 | 投递→查看→邀面试→录用,每步状态可见 | 双端同时打开,实时刷新进度 |
| 非法流转 | 已拒绝的投递不能再改录用 | 直接调接口传目标状态,看是否报错 |
| 过期任务 | 改系统时间或手动触发,过期岗位不再出现 | 手动执行定时任务后刷新列表 |
答辩被追问最多的三个点,提前准备好答案。第一个是“为什么用 SSM 还要升级 SpringBoot”,答:SSM 阶段是为了把分层和装配关系讲清楚,升级后业务代码零改动,体现的是可维护性而不是追新。第二个是“并发投递怎么保证不重复”,答:唯一索引兜底 + 捕获异常转业务提示,不要说“我加了锁”,锁的粒度和释放时机一问就露馅。第三个是“数据量大了怎么办”,答:给出当前方案的数据量边界和下一步演进方向,比如检索换全文索引、列表加 Redis 缓存,别硬撑说性能没问题。
我自己的习惯是:不管功能多简单,验收前一定用两个不同角色的账号把关键路径各走一遍,因为单账号测试永远发现不了权限越界。这个习惯是被坑出来的——有次演示时用管理员账号点了一圈全部正常,换成普通用户立刻 500,原因就是拦截器里漏了一个角色判断。从那以后我每次交付前都强制走一遍“双角色交叉验证”,宁可多花二十分钟,也不在上台时翻车。希望这套结构和清单能帮到你,需要对照源码和建表脚本改的话,可以直接拿去用。
本文还有配套的精品资源,点击获取