二哥项目资料

求职派业务架构设计:用户认证与权限管理、校招信息采集 Agent、异步 AI 调度任务、微信支付、全局字典

正文 21,081 字 #03 原文
本文目录 · 39 节

星球:Java进阶之路&二哥编程星球 | 专栏:求职派 OpenClaw 架构 | 作者:沉默王二 | 发布:2026-07-03

求职派业务架构设计:用户认证与权限管理、校招信息采集 Agent、异步 AI 调度任务、微信支付、全局字典

求职派业务架构设计:用户认证与权限管理、校招信息采集 Agent、异步 AI 调度任务、微信支付、全局字典

很多小伙伴能写代码,但面试时被问到“你的项目架构是怎么设计的”,就说不清楚了。不是技术不行,是从来没练过“从需求到架构图”怎么去表达。

这篇内容会拿求职派的真实业务做示范,从需求描述开始,经过业务流程梳理、功能模块拆分、分层架构设计等三个阶段,最终输出一张完整的业务架构图。

技术派: https://paicoding.com/jobclaw-architecture-deconstruction

图片.png

求职派的原始需求来自技术派的一篇帖子( 原始需求),目标很明确:做一个面向应届生的信息聚合平台。

图片.png

基于这个目标,我对整个产品做了第一期的功能拆分,一共三块:

本篇内容第一版的作者:星球嘉宾灰灰。二哥做了二期的优化和迭代。

  • 用户:登录 + 会员付费

  • 校招信息展示:前台浏览页 + 后台管理页

  • 校招信息采集:基于 Spring AI + 大模型实现校招信息自动录入

三件事,对应三类角色。

普通用户看信息,管理员维护信息,大模型采集信息。围绕这三块,我画了核心业务的主线流程图:

图片.png

这张图是在动手写代码之前就和二哥做了业务确认,最终实现完全吻合(2025年就规划了,第一期也是2025年就完成了,但到了2026年,AI的发展突飞猛进,感觉一期的交付内容已经落后,于是我们又马不停蹄做了二期优化,加入了OpenClaw架构)。

架构设计的第一步,把业务主线理清楚——我们得先知道自己要做什么,才能决定怎么做。

01、从主线到功能模块

初版功能视图

从实际业务出发,核心功能围绕“用户”和“求职信息采集”两块展开,第一版思维导图长这样:

图片.png

模块细化

把用户这块拆开,登录走 JWT 鉴权,充值走微信支付,各自独立:

图片.png

求职信息采集模块同样一分为二,Spring AI 采集能力和校招数据管理各管各的:

图片.png

模块划分

经过两轮细化,功能模块归为两大类、四小块:用户、会员、校招信息、AI 能力。

从用户视角,还可以分为前台视角和管理员视角:

图片.png

当然,你现在看到的第二期,其实又进化了。

02、五大模块

结合具体的实施策略,我对四小块做了一次调整。

校招信息的人工维护目前主要靠后台表单操作,技术含量不大,从核心模块中拿掉。Spring AI 这块拆成两部分:大模型采集能力和任务调度。

最终整个求职派的业务功能划分为五大模块:

  • 实现登录、鉴权的 用户认证与权限管理

  • 基于 Spring AI + LangGraph4J 实现的 校招信息采集 Agent

  • 支撑采集任务的 异步 AI 调度任务

  • 以微信支付为基座的 会员充值

  • 统一维护全局配置的 业务字典管理

用户认证与权限管理

登录方式是微信扫码登录,配合 JWT 实现无状态身份验证。这个方案不需要用户记密码,扫码即登录。

技术上有个细节可以聊聊:二维码登录用的是 SSE 长连接。用户打开登录页时,后端生成一个验证码,通过内存缓存管理验证码的生命周期,五分钟自动过期。用户扫码后,后端直接通过 SSE 推送登录结果,前端不需要反复问“登录了没”。

Java

// LoginService.java
public SseEmitter subscribe() throws IOException {
    String deviceId = ReqInfoContext.getReqInfo().getDeviceId();
    String realCode = deviceCodeCache.getUnchecked(deviceId);
    SseEmitter sseEmitter = new SseEmitter(SSE_EXPIRE_TIME);
    verifyCodeCache.put(realCode, sseEmitter);
    sseEmitter.send("qr#" + genLoginQrImg(realCode));
    return sseEmitter;
}
  • 拿设备标识生成验证码

  • 创建 SSE 连接

  • 推送二维码图片。

权限管理用的是自定义注解加拦截器。在接口上标注权限注解并指定角色级别,拦截器自动校验当前用户是否匹配,不匹配直接返回 403。

三种角色:普通用户、VIP、管理员。MCP 接口也走同一套校验逻辑,从请求头里取 Bearer Token 做身份识别。

按照分层架构的视图,把用户认证和权限管理的内容填进去,架构图变成这样:

图片.png

信息采集 Agent

这是求职派技术含量最高的模块。

采集任务提交后,系统在任务表插入一条记录,然后在后台异步执行。

支持六种数据输入方式:纯文本、HTML 富文本、HTTP 链接、CSV 文件、Excel 文件、图片。

不管校招信息发在哪——微信公众号、学校官网、Excel 表格——都能采集。

采集 Agent 用 LangGraph4J 编排,整个工作流是五个节点的有向图:录入任务、任务分类、数据采集、草稿清洗、信息发布。

图片.png

节点之间是条件边——如果上一步没拿到有效数据,直接中止,不浪费后续的计算资源。

Java

// AgentExecutor.java
return new StateGraph<>(OcAgentState.serializer())
    .addNode("TaskClassify", node_async(taskClassifyAgent::apply))
    .addNode("TaskGather",   node_async(taskGatherAgent::apply))
    .addNode("DraftWasher",  node_async(draftWasherAgent::apply))
    .addNode("DraftPublish", node_async(draftPublishAgent::apply))
    .addEdge(START, "TaskClassify")
    .addConditionalEdges("TaskClassify",
        edge_async(s -> s.getTask() == null ? END : "采集"),
        EdgeMappings.builder().to("TaskGather","采集").toEND().build());

任务分类节点会自动识别输入数据的类型——根据文件后缀判断是 CSV 还是 Excel,根据内容是否包含 HTML 标签判断是富文本还是纯文本,根据是否以 http 开头判断是链接还是文本。

分类失败时条件边直接导向终点,整个流程不会因为一个输入有问题直接卡死。

采集过程中有这样一个处理:大模型有时会因为输出长度限制,从而截断响应,系统通过检测响应末尾是否正常闭合来判断是否需要追加对话。直到拿到完整的结构化数据为止。

在架构图上,这块内容对应应用层的“任务录入”和领域层的“采集领域”,采集结果映射为草稿实体:

图片.png

异步任务调度

大模型的响应速度通常比较慢,有时候可能需要几分钟,甚至几十分钟,因为要 ReAct,经过很多轮推理和工具调用。

所以我们的解决方案是设计了一个任务池:用户提交的采集任务先写入数据库,状态标记为“未处理”,然后由后台定时任务每五分钟扫一次表,捞出待处理的任务逐条执行。执行完毕后更新状态为“成功”或“失败”,前端通过查询接口获取结果。

图片.png

整个调度机制用 Spring 的事件驱动实现。

任务入库时发布一个状态变更事件,监听器收到事件后立刻触发调度。同时定时任务每五分钟兜底扫一次,防止事件丢失。双保险。

Java

// OfferGatherService.java
@Async
@EventListener(TaskChangeListener.class)
public void taskListener(TaskChangeListener listener) {
    switch (listener.getState()) {
        case INIT -> scheduleToLoadTask();
        case SUCCEED, FAILED -> {
            log.info("任务执行完成,通知用户");
        }
    }
}

这里有个设计选择:调度器用原子布尔变量做了单线程锁,同一时间只允许一个线程执行采集任务。

这是为了避免多个定时任务并发扫表导致同一条任务被重复执行的问题。大模型调用本身是 IO 密集型操作,多线程并发调用并不会显著提速,反而增加了 Token 消耗和错误处理的复杂度。

任务数据模型里有一个重试计数字段,失败的任务可以手动重置状态后重新调度。

这套机制保证了采集流程的可靠性——即使大模型临时返回乱码,任务不会丢,重跑一次就行。

架构图上新增任务管理相关的内容:

图片.png

会员充值

集成微信支付实现会员充值,支持四个等级:月卡、季卡、年卡、终身会员。

支付流程走的是微信 Native 支付。后端向微信创建订单拿到预支付 ID,前端展示支付二维码,用户扫码付款后微信回调通知后端,后端更新充值状态并刷新用户的 VIP 等级。

这里有两个防御性设计。

一是幂等性检查:同一笔订单的回调可能因为网络问题被微信多次推送,后端通过数据库行锁加状态判断保证只处理一次。

二是超时释放:每五分钟扫一次未完成的订单,超过预支付有效期(十分钟)的自动标记为失败,释放占用的资源。

会员价格从全局字典中动态读取。运营调价时只需要改字典,不用改代码、不用重新部署。

架构图上新增微信支付和会员充值:

图片.png

全局字典

全局字典解决的是前后端数据同步问题。

校招信息里有很多预定义字段——公司类型、招聘对象、学历要求。

如果用硬编码,前端维护一套,后端维护一套,改一个地方忘改另一个,映射就对不上了。更麻烦的是,这些字段的取值并不固定,公司类型可能随时新增,招聘对象的分类也可能调整。

图片.png

一张表,按应用和键分组存储配置项。前端启动时调一个接口拉取全部公开字典,后续所有下拉选项、枚举值都从字典里取。运营在后台改了字典值,前端刷新页面即生效,不需要发版。

除了业务字段的枚举值,字典还承担了系统配置的职责。

VIP 各等级价格、支持的大模型列表、站点环境标识,都通过字典统一管理。这让系统的可配置项集中在一个地方,避免魔法数字散落在代码各处。

架构图上补齐全局字典:

图片.png

完整架构全貌

五大模块补齐后,再加上接入层的统一封装(请求上下文管理、跨域处理、统一响应格式),最终的业务架构图如下:

图片.png

看完这张图,你就能说出核心功能模块有哪些、关键技术栈是什么、数据怎么流转。

03、架构背后的设计

事件驱动还是消息队列?

任务调度用的是 Spring 事件机制,而不是 RabbitMQ 或 Kafka。当前的业务量不需要分布式消息中间件,单机事件足够,部署复杂度低得多。如果将来采集量上来了,把事件发布换成消息队列,改动集中在一个地方,迁移成本可控。

单线程调度还是线程池?

采集任务的调度用原子变量锁保证同一时间只有一个线程在跑。大模型调用是 IO 密集型操作,多线程并发调用不会显著提速,反而增加 Token 消耗和错误处理的复杂度。对当前的业务规模来说,简单可靠比高并发更重要。

字典还是配置文件?

VIP 价格放在数据库字典里而不是 application.yml 里。配置文件改一次要重启服务,字典改完即时生效。对于运营频繁调整的参数,数据库方案的运维成本远低于配置文件方案。

这些取舍没有标准答案。但在面试中能说出“我选了 A 而不是 B,因为在当前场景下 A 的优势更重要”,比单纯描述架构本身更有说服力。

04、求职派如何写简历上?

项目名称:求职派 — 基于 Spring AI+LangGraph4J 的 OpenClaw 架构信息聚合平台

项目简介:面向校招场景的信息聚合平台,支持大模型驱动的校招信息自动采集,集成微信生态实现用户认证与会员充值。

技术栈:Java 21 / Spring Boot 4.0 / Spring AI 2.0 / LangGraph4J / JWT / SSE / 微信支付 / H2 / React 19 / Next.js 15

核心职责:

  • 基于 LangGraph4J 实现多节点工作流,完成校招信息从分类、采集、清洗到发布的全自动化化,支持文本、文件、图片、网页等六种源数据录入

  • 设计事件驱动的异步任务调度机制,采用定时扫表加事件监听,保证大模型调用的可靠性和任务执行的幂等性

  • 集成微信 Native 支付实现会员充值系统,通过行锁加状态机保证支付回调的幂等性

  • 实现基于 SSE 长连接的微信公众号扫码登录,配合 JWT 无状态鉴权和自定义权限注解实现三级权限管理

  • 设计全局字典系统统一管理前后端配置项,实现业务参数的动态更新,消除硬编码导致的前后端数据不一致问题

很多小伙伴能写代码,但面试时被问到“你的项目架构是怎么设计的”,就说不清楚了。不是技术不行,是从来没练过“从需求到架构图”怎么去表达。

这篇内容会拿求职派的真实业务做示范,从需求描述开始,经过业务流程梳理、功能模块拆分、分层架构设计等三个阶段,最终输出一张完整的业务架构图。

技术派: https://paicoding.com/jobclaw-architecture-deconstruction

图片.png

求职派的原始需求来自技术派的一篇帖子( 原始需求),目标很明确:做一个面向应届生的信息聚合平台。

图片.png

基于这个目标,我对整个产品做了第一期的功能拆分,一共三块:

本篇内容第一版的作者:星球嘉宾灰灰。二哥做了二期的优化和迭代。

  • 用户:登录 + 会员付费

  • 校招信息展示:前台浏览页 + 后台管理页

  • 校招信息采集:基于 Spring AI + 大模型实现校招信息自动录入

三件事,对应三类角色。

普通用户看信息,管理员维护信息,大模型采集信息。围绕这三块,我画了核心业务的主线流程图:

图片.png

这张图是在动手写代码之前就和二哥做了业务确认,最终实现完全吻合(2025年就规划了,第一期也是2025年就完成了,但到了2026年,AI的发展突飞猛进,感觉一期的交付内容已经落后,于是我们又马不停蹄做了二期优化,加入了OpenClaw架构)。

架构设计的第一步,把业务主线理清楚——我们得先知道自己要做什么,才能决定怎么做。

01、从主线到功能模块

初版功能视图

从实际业务出发,核心功能围绕“用户”和“求职信息采集”两块展开,第一版思维导图长这样:

图片.png

模块细化

把用户这块拆开,登录走 JWT 鉴权,充值走微信支付,各自独立:

图片.png

求职信息采集模块同样一分为二,Spring AI 采集能力和校招数据管理各管各的:

图片.png

模块划分

经过两轮细化,功能模块归为两大类、四小块:用户、会员、校招信息、AI 能力。

从用户视角,还可以分为前台视角和管理员视角:

图片.png

当然,你现在看到的第二期,其实又进化了。

02、五大模块

结合具体的实施策略,我对四小块做了一次调整。

校招信息的人工维护目前主要靠后台表单操作,技术含量不大,从核心模块中拿掉。Spring AI 这块拆成两部分:大模型采集能力和任务调度。

最终整个求职派的业务功能划分为五大模块:

  • 实现登录、鉴权的 用户认证与权限管理

  • 基于 Spring AI + LangGraph4J 实现的 校招信息采集 Agent

  • 支撑采集任务的 异步 AI 调度任务

  • 以微信支付为基座的 会员充值

  • 统一维护全局配置的 业务字典管理

用户认证与权限管理

登录方式是微信扫码登录,配合 JWT 实现无状态身份验证。这个方案不需要用户记密码,扫码即登录。

技术上有个细节可以聊聊:二维码登录用的是 SSE 长连接。用户打开登录页时,后端生成一个验证码,通过内存缓存管理验证码的生命周期,五分钟自动过期。用户扫码后,后端直接通过 SSE 推送登录结果,前端不需要反复问“登录了没”。

Java

// LoginService.java
public SseEmitter subscribe() throws IOException {
    String deviceId = ReqInfoContext.getReqInfo().getDeviceId();
    String realCode = deviceCodeCache.getUnchecked(deviceId);
    SseEmitter sseEmitter = new SseEmitter(SSE_EXPIRE_TIME);
    verifyCodeCache.put(realCode, sseEmitter);
    sseEmitter.send("qr#" + genLoginQrImg(realCode));
    return sseEmitter;
}
  • 拿设备标识生成验证码

  • 创建 SSE 连接

  • 推送二维码图片。

权限管理用的是自定义注解加拦截器。在接口上标注权限注解并指定角色级别,拦截器自动校验当前用户是否匹配,不匹配直接返回 403。

三种角色:普通用户、VIP、管理员。MCP 接口也走同一套校验逻辑,从请求头里取 Bearer Token 做身份识别。

按照分层架构的视图,把用户认证和权限管理的内容填进去,架构图变成这样:

图片.png

信息采集 Agent

这是求职派技术含量最高的模块。

采集任务提交后,系统在任务表插入一条记录,然后在后台异步执行。

支持六种数据输入方式:纯文本、HTML 富文本、HTTP 链接、CSV 文件、Excel 文件、图片。

不管校招信息发在哪——微信公众号、学校官网、Excel 表格——都能采集。

采集 Agent 用 LangGraph4J 编排,整个工作流是五个节点的有向图:录入任务、任务分类、数据采集、草稿清洗、信息发布。

图片.png

节点之间是条件边——如果上一步没拿到有效数据,直接中止,不浪费后续的计算资源。

Java

// AgentExecutor.java
return new StateGraph<>(OcAgentState.serializer())
    .addNode("TaskClassify", node_async(taskClassifyAgent::apply))
    .addNode("TaskGather",   node_async(taskGatherAgent::apply))
    .addNode("DraftWasher",  node_async(draftWasherAgent::apply))
    .addNode("DraftPublish", node_async(draftPublishAgent::apply))
    .addEdge(START, "TaskClassify")
    .addConditionalEdges("TaskClassify",
        edge_async(s -> s.getTask() == null ? END : "采集"),
        EdgeMappings.builder().to("TaskGather","采集").toEND().build());

任务分类节点会自动识别输入数据的类型——根据文件后缀判断是 CSV 还是 Excel,根据内容是否包含 HTML 标签判断是富文本还是纯文本,根据是否以 http 开头判断是链接还是文本。

分类失败时条件边直接导向终点,整个流程不会因为一个输入有问题直接卡死。

采集过程中有这样一个处理:大模型有时会因为输出长度限制,从而截断响应,系统通过检测响应末尾是否正常闭合来判断是否需要追加对话。直到拿到完整的结构化数据为止。

在架构图上,这块内容对应应用层的“任务录入”和领域层的“采集领域”,采集结果映射为草稿实体:

图片.png

异步任务调度

大模型的响应速度通常比较慢,有时候可能需要几分钟,甚至几十分钟,因为要 ReAct,经过很多轮推理和工具调用。

所以我们的解决方案是设计了一个任务池:用户提交的采集任务先写入数据库,状态标记为“未处理”,然后由后台定时任务每五分钟扫一次表,捞出待处理的任务逐条执行。执行完毕后更新状态为“成功”或“失败”,前端通过查询接口获取结果。

图片.png

整个调度机制用 Spring 的事件驱动实现。

任务入库时发布一个状态变更事件,监听器收到事件后立刻触发调度。同时定时任务每五分钟兜底扫一次,防止事件丢失。双保险。

Java

// OfferGatherService.java
@Async
@EventListener(TaskChangeListener.class)
public void taskListener(TaskChangeListener listener) {
    switch (listener.getState()) {
        case INIT -> scheduleToLoadTask();
        case SUCCEED, FAILED -> {
            log.info("任务执行完成,通知用户");
        }
    }
}

这里有个设计选择:调度器用原子布尔变量做了单线程锁,同一时间只允许一个线程执行采集任务。

这是为了避免多个定时任务并发扫表导致同一条任务被重复执行的问题。大模型调用本身是 IO 密集型操作,多线程并发调用并不会显著提速,反而增加了 Token 消耗和错误处理的复杂度。

任务数据模型里有一个重试计数字段,失败的任务可以手动重置状态后重新调度。

这套机制保证了采集流程的可靠性——即使大模型临时返回乱码,任务不会丢,重跑一次就行。

架构图上新增任务管理相关的内容:

图片.png

会员充值

集成微信支付实现会员充值,支持四个等级:月卡、季卡、年卡、终身会员。

支付流程走的是微信 Native 支付。后端向微信创建订单拿到预支付 ID,前端展示支付二维码,用户扫码付款后微信回调通知后端,后端更新充值状态并刷新用户的 VIP 等级。

这里有两个防御性设计。

一是幂等性检查:同一笔订单的回调可能因为网络问题被微信多次推送,后端通过数据库行锁加状态判断保证只处理一次。

二是超时释放:每五分钟扫一次未完成的订单,超过预支付有效期(十分钟)的自动标记为失败,释放占用的资源。

会员价格从全局字典中动态读取。运营调价时只需要改字典,不用改代码、不用重新部署。

架构图上新增微信支付和会员充值:

图片.png

全局字典

全局字典解决的是前后端数据同步问题。

校招信息里有很多预定义字段——公司类型、招聘对象、学历要求。

如果用硬编码,前端维护一套,后端维护一套,改一个地方忘改另一个,映射就对不上了。更麻烦的是,这些字段的取值并不固定,公司类型可能随时新增,招聘对象的分类也可能调整。

图片.png

一张表,按应用和键分组存储配置项。前端启动时调一个接口拉取全部公开字典,后续所有下拉选项、枚举值都从字典里取。运营在后台改了字典值,前端刷新页面即生效,不需要发版。

除了业务字段的枚举值,字典还承担了系统配置的职责。

VIP 各等级价格、支持的大模型列表、站点环境标识,都通过字典统一管理。这让系统的可配置项集中在一个地方,避免魔法数字散落在代码各处。

架构图上补齐全局字典:

图片.png

完整架构全貌

五大模块补齐后,再加上接入层的统一封装(请求上下文管理、跨域处理、统一响应格式),最终的业务架构图如下:

图片.png

看完这张图,你就能说出核心功能模块有哪些、关键技术栈是什么、数据怎么流转。

03、架构背后的设计

事件驱动还是消息队列?

任务调度用的是 Spring 事件机制,而不是 RabbitMQ 或 Kafka。当前的业务量不需要分布式消息中间件,单机事件足够,部署复杂度低得多。如果将来采集量上来了,把事件发布换成消息队列,改动集中在一个地方,迁移成本可控。

单线程调度还是线程池?

采集任务的调度用原子变量锁保证同一时间只有一个线程在跑。大模型调用是 IO 密集型操作,多线程并发调用不会显著提速,反而增加 Token 消耗和错误处理的复杂度。对当前的业务规模来说,简单可靠比高并发更重要。

字典还是配置文件?

VIP 价格放在数据库字典里而不是 application.yml 里。配置文件改一次要重启服务,字典改完即时生效。对于运营频繁调整的参数,数据库方案的运维成本远低于配置文件方案。

这些取舍没有标准答案。但在面试中能说出“我选了 A 而不是 B,因为在当前场景下 A 的优势更重要”,比单纯描述架构本身更有说服力。

04、求职派如何写简历上?

项目名称:求职派 — 基于 Spring AI+LangGraph4J 的 OpenClaw 架构信息聚合平台

项目简介:面向校招场景的信息聚合平台,支持大模型驱动的校招信息自动采集,集成微信生态实现用户认证与会员充值。

技术栈:Java 21 / Spring Boot 4.0 / Spring AI 2.0 / LangGraph4J / JWT / SSE / 微信支付 / H2 / React 19 / Next.js 15

核心职责:

  • 基于 LangGraph4J 实现多节点工作流,完成校招信息从分类、采集、清洗到发布的全自动化化,支持文本、文件、图片、网页等六种源数据录入

  • 设计事件驱动的异步任务调度机制,采用定时扫表加事件监听,保证大模型调用的可靠性和任务执行的幂等性

  • 集成微信 Native 支付实现会员充值系统,通过行锁加状态机保证支付回调的幂等性

  • 实现基于 SSE 长连接的微信公众号扫码登录,配合 JWT 无状态鉴权和自定义权限注解实现三级权限管理

  • 设计全局字典系统统一管理前后端配置项,实现业务参数的动态更新,消除硬编码导致的前后端数据不一致问题

很多小伙伴能写代码,但面试时被问到“你的项目架构是怎么设计的”,就说不清楚了。不是技术不行,是从来没练过“从需求到架构图”怎么去表达。

这篇内容会拿求职派的真实业务做示范,从需求描述开始,经过业务流程梳理、功能模块拆分、分层架构设计等三个阶段,最终输出一张完整的业务架构图。

技术派: https://paicoding.com/jobclaw-architecture-deconstruction

图片.png

求职派的原始需求来自技术派的一篇帖子( 原始需求),目标很明确:做一个面向应届生的信息聚合平台。

图片.png

基于这个目标,我对整个产品做了第一期的功能拆分,一共三块:

本篇内容第一版的作者:星球嘉宾灰灰。二哥做了二期的优化和迭代。

  • 用户:登录 + 会员付费

  • 校招信息展示:前台浏览页 + 后台管理页

  • 校招信息采集:基于 Spring AI + 大模型实现校招信息自动录入

三件事,对应三类角色。

普通用户看信息,管理员维护信息,大模型采集信息。围绕这三块,我画了核心业务的主线流程图:

图片.png

这张图是在动手写代码之前就和二哥做了业务确认,最终实现完全吻合(2025年就规划了,第一期也是2025年就完成了,但到了2026年,AI的发展突飞猛进,感觉一期的交付内容已经落后,于是我们又马不停蹄做了二期优化,加入了OpenClaw架构)。

架构设计的第一步,把业务主线理清楚——我们得先知道自己要做什么,才能决定怎么做。

01、从主线到功能模块

初版功能视图

从实际业务出发,核心功能围绕“用户”和“求职信息采集”两块展开,第一版思维导图长这样:

图片.png

模块细化

把用户这块拆开,登录走 JWT 鉴权,充值走微信支付,各自独立:

图片.png

求职信息采集模块同样一分为二,Spring AI 采集能力和校招数据管理各管各的:

图片.png

模块划分

经过两轮细化,功能模块归为两大类、四小块:用户、会员、校招信息、AI 能力。

从用户视角,还可以分为前台视角和管理员视角:

图片.png

当然,你现在看到的第二期,其实又进化了。

02、五大模块

结合具体的实施策略,我对四小块做了一次调整。

校招信息的人工维护目前主要靠后台表单操作,技术含量不大,从核心模块中拿掉。Spring AI 这块拆成两部分:大模型采集能力和任务调度。

最终整个求职派的业务功能划分为五大模块:

  • 实现登录、鉴权的 用户认证与权限管理

  • 基于 Spring AI + LangGraph4J 实现的 校招信息采集 Agent

  • 支撑采集任务的 异步 AI 调度任务

  • 以微信支付为基座的 会员充值

  • 统一维护全局配置的 业务字典管理

用户认证与权限管理

登录方式是微信扫码登录,配合 JWT 实现无状态身份验证。这个方案不需要用户记密码,扫码即登录。

技术上有个细节可以聊聊:二维码登录用的是 SSE 长连接。用户打开登录页时,后端生成一个验证码,通过内存缓存管理验证码的生命周期,五分钟自动过期。用户扫码后,后端直接通过 SSE 推送登录结果,前端不需要反复问“登录了没”。

Java

// LoginService.java
public SseEmitter subscribe() throws IOException {
    String deviceId = ReqInfoContext.getReqInfo().getDeviceId();
    String realCode = deviceCodeCache.getUnchecked(deviceId);
    SseEmitter sseEmitter = new SseEmitter(SSE_EXPIRE_TIME);
    verifyCodeCache.put(realCode, sseEmitter);
    sseEmitter.send("qr#" + genLoginQrImg(realCode));
    return sseEmitter;
}
  • 拿设备标识生成验证码

  • 创建 SSE 连接

  • 推送二维码图片。

权限管理用的是自定义注解加拦截器。在接口上标注权限注解并指定角色级别,拦截器自动校验当前用户是否匹配,不匹配直接返回 403。

三种角色:普通用户、VIP、管理员。MCP 接口也走同一套校验逻辑,从请求头里取 Bearer Token 做身份识别。

按照分层架构的视图,把用户认证和权限管理的内容填进去,架构图变成这样:

图片.png

信息采集 Agent

这是求职派技术含量最高的模块。

采集任务提交后,系统在任务表插入一条记录,然后在后台异步执行。

支持六种数据输入方式:纯文本、HTML 富文本、HTTP 链接、CSV 文件、Excel 文件、图片。

不管校招信息发在哪——微信公众号、学校官网、Excel 表格——都能采集。

采集 Agent 用 LangGraph4J 编排,整个工作流是五个节点的有向图:录入任务、任务分类、数据采集、草稿清洗、信息发布。

图片.png

节点之间是条件边——如果上一步没拿到有效数据,直接中止,不浪费后续的计算资源。

Java

// AgentExecutor.java
return new StateGraph<>(OcAgentState.serializer())
    .addNode("TaskClassify", node_async(taskClassifyAgent::apply))
    .addNode("TaskGather",   node_async(taskGatherAgent::apply))
    .addNode("DraftWasher",  node_async(draftWasherAgent::apply))
    .addNode("DraftPublish", node_async(draftPublishAgent::apply))
    .addEdge(START, "TaskClassify")
    .addConditionalEdges("TaskClassify",
        edge_async(s -> s.getTask() == null ? END : "采集"),
        EdgeMappings.builder().to("TaskGather","采集").toEND().build());

任务分类节点会自动识别输入数据的类型——根据文件后缀判断是 CSV 还是 Excel,根据内容是否包含 HTML 标签判断是富文本还是纯文本,根据是否以 http 开头判断是链接还是文本。

分类失败时条件边直接导向终点,整个流程不会因为一个输入有问题直接卡死。

采集过程中有这样一个处理:大模型有时会因为输出长度限制,从而截断响应,系统通过检测响应末尾是否正常闭合来判断是否需要追加对话。直到拿到完整的结构化数据为止。

在架构图上,这块内容对应应用层的“任务录入”和领域层的“采集领域”,采集结果映射为草稿实体:

图片.png

异步任务调度

大模型的响应速度通常比较慢,有时候可能需要几分钟,甚至几十分钟,因为要 ReAct,经过很多轮推理和工具调用。

所以我们的解决方案是设计了一个任务池:用户提交的采集任务先写入数据库,状态标记为“未处理”,然后由后台定时任务每五分钟扫一次表,捞出待处理的任务逐条执行。执行完毕后更新状态为“成功”或“失败”,前端通过查询接口获取结果。

图片.png

整个调度机制用 Spring 的事件驱动实现。

任务入库时发布一个状态变更事件,监听器收到事件后立刻触发调度。同时定时任务每五分钟兜底扫一次,防止事件丢失。双保险。

Java

// OfferGatherService.java
@Async
@EventListener(TaskChangeListener.class)
public void taskListener(TaskChangeListener listener) {
    switch (listener.getState()) {
        case INIT -> scheduleToLoadTask();
        case SUCCEED, FAILED -> {
            log.info("任务执行完成,通知用户");
        }
    }
}

这里有个设计选择:调度器用原子布尔变量做了单线程锁,同一时间只允许一个线程执行采集任务。

这是为了避免多个定时任务并发扫表导致同一条任务被重复执行的问题。大模型调用本身是 IO 密集型操作,多线程并发调用并不会显著提速,反而增加了 Token 消耗和错误处理的复杂度。

任务数据模型里有一个重试计数字段,失败的任务可以手动重置状态后重新调度。

这套机制保证了采集流程的可靠性——即使大模型临时返回乱码,任务不会丢,重跑一次就行。

架构图上新增任务管理相关的内容:

图片.png

会员充值

集成微信支付实现会员充值,支持四个等级:月卡、季卡、年卡、终身会员。

支付流程走的是微信 Native 支付。后端向微信创建订单拿到预支付 ID,前端展示支付二维码,用户扫码付款后微信回调通知后端,后端更新充值状态并刷新用户的 VIP 等级。

这里有两个防御性设计。

一是幂等性检查:同一笔订单的回调可能因为网络问题被微信多次推送,后端通过数据库行锁加状态判断保证只处理一次。

二是超时释放:每五分钟扫一次未完成的订单,超过预支付有效期(十分钟)的自动标记为失败,释放占用的资源。

会员价格从全局字典中动态读取。运营调价时只需要改字典,不用改代码、不用重新部署。

架构图上新增微信支付和会员充值:

图片.png

全局字典

全局字典解决的是前后端数据同步问题。

校招信息里有很多预定义字段——公司类型、招聘对象、学历要求。

如果用硬编码,前端维护一套,后端维护一套,改一个地方忘改另一个,映射就对不上了。更麻烦的是,这些字段的取值并不固定,公司类型可能随时新增,招聘对象的分类也可能调整。

图片.png

一张表,按应用和键分组存储配置项。前端启动时调一个接口拉取全部公开字典,后续所有下拉选项、枚举值都从字典里取。运营在后台改了字典值,前端刷新页面即生效,不需要发版。

除了业务字段的枚举值,字典还承担了系统配置的职责。

VIP 各等级价格、支持的大模型列表、站点环境标识,都通过字典统一管理。这让系统的可配置项集中在一个地方,避免魔法数字散落在代码各处。

架构图上补齐全局字典:

图片.png

完整架构全貌

五大模块补齐后,再加上接入层的统一封装(请求上下文管理、跨域处理、统一响应格式),最终的业务架构图如下:

图片.png

看完这张图,你就能说出核心功能模块有哪些、关键技术栈是什么、数据怎么流转。

03、架构背后的设计

事件驱动还是消息队列?

任务调度用的是 Spring 事件机制,而不是 RabbitMQ 或 Kafka。当前的业务量不需要分布式消息中间件,单机事件足够,部署复杂度低得多。如果将来采集量上来了,把事件发布换成消息队列,改动集中在一个地方,迁移成本可控。

单线程调度还是线程池?

采集任务的调度用原子变量锁保证同一时间只有一个线程在跑。大模型调用是 IO 密集型操作,多线程并发调用不会显著提速,反而增加 Token 消耗和错误处理的复杂度。对当前的业务规模来说,简单可靠比高并发更重要。

字典还是配置文件?

VIP 价格放在数据库字典里而不是 application.yml 里。配置文件改一次要重启服务,字典改完即时生效。对于运营频繁调整的参数,数据库方案的运维成本远低于配置文件方案。

这些取舍没有标准答案。但在面试中能说出“我选了 A 而不是 B,因为在当前场景下 A 的优势更重要”,比单纯描述架构本身更有说服力。

04、求职派如何写简历上?

项目名称:求职派 — 基于 Spring AI+LangGraph4J 的 OpenClaw 架构信息聚合平台

项目简介:面向校招场景的信息聚合平台,支持大模型驱动的校招信息自动采集,集成微信生态实现用户认证与会员充值。

技术栈:Java 21 / Spring Boot 4.0 / Spring AI 2.0 / LangGraph4J / JWT / SSE / 微信支付 / H2 / React 19 / Next.js 15

核心职责:

  • 基于 LangGraph4J 实现多节点工作流,完成校招信息从分类、采集、清洗到发布的全自动化化,支持文本、文件、图片、网页等六种源数据录入

  • 设计事件驱动的异步任务调度机制,采用定时扫表加事件监听,保证大模型调用的可靠性和任务执行的幂等性

  • 集成微信 Native 支付实现会员充值系统,通过行锁加状态机保证支付回调的幂等性

  • 实现基于 SSE 长连接的微信公众号扫码登录,配合 JWT 无状态鉴权和自定义权限注解实现三级权限管理

  • 设计全局字典系统统一管理前后端配置项,实现业务参数的动态更新,消除硬编码导致的前后端数据不一致问题