1. 为什么做一个论文投稿系统
先交代一下背景。这个项目我内部代号叫"m231论文投稿系统",起因是所在团队一直用邮箱加Excel表格来管理内部论文投稿和审稿,每次到了集中投稿期,邮箱里几十封附件来回飞,版本混乱、审稿进度全靠手动催,统计工作量更是痛苦。所以决定自研一套轻量投稿系统,把从作者投稿、格式审查、分配审稿人、提交审稿意见,到最后出录用结论的整个流程,全部搬到线上。
这篇文章适合谁看?如果你正准备开发类似的流程管理系统——不只是论文投稿,包括内部项目申报、工单流转、评审打分这类"多人协作+多阶段流转"的系统,都可以参考。这里不只是一个功能清单,而是把每一步的设计权衡、踩过的坑、最后的实现方案都讲清楚,很多细节是需求文档里不会写的。
系统最大的一个特点,是把"状态流转"作为核心引擎。我没把业务逻辑散落在各个接口里,而是集中到一个状态机模块里统一管理,配合数据库锁来做并发控制。这一点后面会展开细讲,也是整个项目里性价比最高的一个设计决策。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 需求拆解:先把"投稿"这件事想透
2.1 核心角色的职责边界
做这套系统前,我把参与方拆成了四类角色:作者、编辑、审稿人、系统管理员。听起来很简单,但每一类角色在真实场景里的诉求其实差异很大。
- 作者关心的是"我的论文到哪一步了"、"意见什么时候回来"、"返修后有没有被正确接收",最怕的是系统没有任何反馈,投完像石沉大海。
- 编辑是系统的重度使用者,他们关心的是批量分配审稿人是否方便、格式审查是否高效、催审提醒能不能自动触发、月底统计能不能一键导出。
- 审稿人需要的是一个清爽的工作台——待审列表、审稿表单、截止时间提醒,不需要也不应该看到其他审稿人的意见。
- 管理员则关注账号安全、操作审计、数据备份这类平台级事务。
权限体系就是围绕这四个角色来做的,核心原则是"最小可见性":每个角色只看到自己职责范围内该看的东西。这个原则在执行中会遇到很多边界情况,后面会在权限部分专门讲。
2.2 流程节点与状态设计
投稿系统中,一篇论文的生命周期看起来是一条固定的流水线:提交、查重/格式审查、分配审稿人、外审、返修、终审、录用/拒稿。但真做起来,任何一步都可能被驳回重来,所以流程不是一条直线,而是一张带回路的状态图。
我最终把状态节点收敛为这些:
| 状态 | 含义 | 可流转至 |
|---|---|---|
| DRAFT | 作者保存草稿 | SUBMITTED |
| SUBMITTED | 已提交,待编辑处理 | UNDER_REVIEW, REJECTED |
| UNDER_REVIEW | 审稿中 | REVISION_REQUIRED, ACCEPTED, REJECTED |
| REVISION_REQUIRED | 需返修 | UNDER_REVIEW, REJECTED |
| ACCEPTED | 录用 | 终态 |
| REJECTED | 拒稿 | 终态 |
为什么不像很多复杂系统那样把"编辑部初筛"和"外审中"拆成十几个状态?因为状态多了,维护成本成倍上涨,而且对使用者来说,太细的状态反而看懵。我的经验是,状态粒度要匹配业务的实际管控颗粒度,如果编辑并不需要区分"审稿分配中"和"等待第一位审稿人返回意见",那就合并成一个 UNDER_REVIEW,具体的中间过程通过子记录去实现。
2.3 核心痛点与解决方案
开发过程中,我提炼出了四个必须优先解决的痛点:
第一,版本混乱。传统邮箱投稿中,作者经常发"更新版""最终版""再改一版",谁也说不清哪份是最新的。解决方案是建立独立的"投稿版本表",每次作者上传新文件都新增一条记录,同时保留历史版本,任何时刻都能定位到系统当前使用的是哪个版本。
第二,审稿人指派不透明。编辑指派了谁、几位审稿人、已返回几份意见,这些信息如果靠人工记忆,必然出错。解决方案是审稿分配独立成表,支持一篇论文挂多个审稿人,每个审稿人一个独立记录,既能看到整体进度,又不会让审稿人之间相互干扰。
第三,截止时间不可控。很多流程卡就卡在"没人催"。解决方案是把截止日期字段化,配合一个定时任务,到期前一天自动给审稿人发提醒,超期未返回则抄送编辑介入。
第四,统计难。每月、每季度要统计投稿量、录用率、平均审稿周期,如果全靠人工台账,效率极低。解决方案是把所有状态变更都写入日志表,任何统计需求都可以通过日志表按时间维度聚合出来。
3. 技术选型:为什么是这些组合
3.1 整体架构方案
m231论文投稿系统采用了前后端分离的Web架构。后端是 Spring Boot 3 + MyBatis-Plus,数据库用 MySQL 8.0,前端是 Vue 3 + Element Plus,部署在单台云服务器上,使用 Nginx 做反向代理。这套组合在中小型业务系统里非常成熟,社区资料丰富,团队上手快,也方便后来者维护。
容量上做了个简单估算:目标规模是注册用户不超过 2000 人,每年投稿论文约 800 篇,单篇论文平均审稿 2-3 轮,按此估算的峰值并发只有几十 TPS,一套标准技术栈完全能扛住,不需要一开始就上微服务、消息队列。做技术选型最忌讳过度设计,能简单解决的问题不要复杂化。
3.2 数据库表结构设计的核心思路
数据库是整个系统最基础的部分。我把核心表分成五组:用户体系、投稿主数据、审稿业务、版本管理、流程日志。下面把每张表说清楚,这些表也是整个项目最值得复用的部分。
用户表(sys_user)
角色字段用字符串存角色编码:"AUTHOR"、"EDITOR"、"REVIEWER"、"ADMIN",一个用户可以拥有多个角色。密码用 BCrypt 加密存储,从源头避免明文密码问题。账号状态字段区分启用/停用,方便管理员处理异常账号。
论文主表(paper)
这是投稿系统的中枢。字段包括:标题、摘要、关键词、投稿作者ID、当前状态、论文编号、投稿时间、更新时间。论文编号是给作者看的业务标识,我用了"年份+流水号"的格式,比如 2025-0001,方便作者报号咨询。主表只存"当前状态",历史状态全部放日志表。
投稿版本表(paper_attachment)
每个版本存文件名、文件存储路径(我用的是本地磁盘存储,生产环境建议换OSS)、上传时间、上传者、版本序号、当前版本标志位。作者每次投稿都是新增一条记录,不覆盖旧文件。这个设计被验证非常实用,尤其返修阶段,能清楚对比每次改了什么。
审稿分配表(review_assignment)
存论文ID、审稿人ID、分配时间、截止时间、状态(待审/已接受/已提交/已逾期)。支持一篇论文多个审稿人,每个审稿人一个独立记录。这里有个设计细节:审稿人可以选择接受或拒绝审稿邀请,如果拒绝,编辑会收到系统通知,可以及时改派。
审稿意见表(review_report)
存审稿人ID、分配ID、评分、推荐结论(录用/修改后录用/修改后重审/拒稿)、详细意见、提交时间。意见表里的文字内容用 TEXT 类型,存储空间预留充足,不限制审稿人的书写长度。
状态日志表(paper_status_log)
存论文ID、从哪个状态来、到哪个状态去、操作人、操作时间、备注。这张表是整个系统的"黑匣子",任何追溯和统计都靠它。
3.3 文件存储与上传规划
论文文件的上传是实现中很容易被低估的部分,实际遇到的坑最多。我最初直接存到应用服务器的"上传目录",后来发现几个隐患:一是重启部署时目录可能丢;二是文件名冲突;三是大文件上传会卡住请求线程。
最终的方案是:
- 文件名改为"日期+UUID+原文件名"的格式,从命名上杜绝重复;
- 上传文件大小限制设置为 50MB,超出时前端直接拦截提示,后端也做了对应校验;
- 文件与数据库记录分两步:先落盘,再写数据库记录,如果数据库写入失败则回滚删除文件,避免产生"孤儿文件";
- 下载时通过后端接口做权限校验,"先鉴权后取文件",避免文件被未授权用户直接访问。
4. 实操过程:从零搭建核心功能模块
4.1 项目初始化与依赖管理
创建 Spring Boot 项目时,我用的是 Spring Initializr,选择的依赖包括:Spring Web、Spring Security、MyBatis-Plus、MySQL Driver、Lombok、Validation。关于 MyBatis-Plus,最大的价值是内置了通用的 CRUD 方法,单表操作基本不用写 XML,省了不少重复的 Mapper 代码。
这里有个特别想提醒的点:Spring Security 的配置要尽早接入,不要等业务代码写完了再补。我见过太多项目,业务代码全写完后才接权限,结果每个接口都要回头改,工作量翻倍。项目初始化时就加上 Security 依赖,哪怕只是先配置一个最简单的登录入口,后续扩展会顺畅很多。
4.2 用户认证与权限落地
登录认证采用的是 JWT(JSON Web Token)方案。用户登录成功后,后端签发一个有效期为 24 小时的 Token,前端把它存在本地存储中,每次请求在 Authorization 头带上。后端通过 Spring Security 的过滤器链统一解析 Token,解析出来的用户信息放入上下文,业务层随时可以拿到当前操作人。
权限控制上,我在接口层用注解做了声明式控制:
java复制@PreAuthorize("hasRole('EDITOR')")
@PostMapping("/papers/{id}/assign")
public Result assignReviewer(@PathVariable Long id, @RequestBody AssignRequest req) {
// 仅编辑角色可调用
}
这个做法非常直观,每个接口的权限一眼就能看出来。但有一个注意点:注解只能控制"谁能调用这个接口",不能控制"这个人能操作哪些数据"。也就是说,A 作者不能修改 B 作者的文章,这种数据级权限需要在业务代码里再校验一层。我的做法是写了一个简单的"资源归属校验工具类",在涉及数据操作的地方统一调用。
4.3 投稿主流程的状态机实现
这是整个系统里我最满意的一部分。状态机模块的核心逻辑是:用一张"状态流转配置表"定义哪些迁移是合法的,然后在执行迁移时统一校验、统一落库、统一写日志。
java复制@Component
public class PaperStateMachine {
private static final Map<String, Set<String>> TRANSITIONS = new HashMap<>();
static {
TRANSITIONS.put("DRAFT", Set.of("SUBMITTED"));
TRANSITIONS.put("SUBMITTED", Set.of("UNDER_REVIEW", "REJECTED"));
TRANSITIONS.put("UNDER_REVIEW", Set.of("REVISION_REQUIRED", "ACCEPTED", "REJECTED"));
TRANSITIONS.put("REVISION_REQUIRED", Set.of("UNDER_REVIEW", "REJECTED"));
}
public synchronized Paper changeState(Paper paper, String targetState, Long operatorId, String remark) {
String current = paper.getStatus();
if (!TRANSITIONS.getOrDefault(current, Set.of()).contains(targetState)) {
throw new IllegalStateException("非法状态流转:" + current + " -> " + targetState);
}
paper.setStatus(targetState);
// 更新 paper 表
// 写入 paper_status_log 表
return paper;
}
}
为什么把状态机单独抽出来而不是在每个 Service 里写判断?因为状态校验的规则散落各处后,早晚会出现"某个接口忘了校验",结果把已拒稿的论文又拉回审稿中,产生脏数据。集中到状态机后,整个系统只有这一条路可以改变论文状态,任何非法流转都会被拦下。这个方法我强烈推荐。
状态机里还加了一个 synchronized 关键字,避免同一篇论文的并发操作。这里要说明一下:单机部署下这样够用,但如果是多实例部署,就需要把锁升级为 Redis 分布式锁,或者直接靠数据库行锁。我当前的部署规模用 JVM 锁就够了,这是一个基于容量评估的合理取舍。
4.4 审稿分配与意见收集
审稿分配功能,是在论文通过编辑初筛后触发。编辑选定论文后,从审稿人列表中选择人员,设定审稿截止日期,点击分配。系统会做两件事:写入 review_assignment 表,向审稿人发送待办通知。
审稿人登录后,可以在"我的审稿"页面看到分配给自己的论文列表。列表项有"接受"和"拒绝"两个按钮——这个设计能减少很多无效工作。如果审稿人点击接受,审稿表单才开放;如果拒绝,系统自动通知编辑改派。
审稿表单包含三个部分:对论文学术价值的评分(1-10 分)、推荐结论(下拉选择)、详细的文字意见。提交时做了表单校验,推荐结论和意见不能为空。这里有一个小设计:评分和结论是必填的,但文字意见字数下限设置为 50 字,防止审稿人只给一句"建议录用"就交差。
4.5 返修与多轮审稿
当审稿结论是"需要修改"时,编辑会发起返修操作,系统把论文状态从 UNDER_REVIEW 改为 REVISION_REQUIRED,同时给作者发送通知,要求上传修改稿。作者上传的新版本会以新记录写入 paper_attachment 表,并自动附带"这是第 N 轮修改稿"的标识。
修改稿上传后,作者点击"提交返修",论文状态回到 UNDER_REVIEW,编辑重新安排审稿。这个流程可以循环多轮。我在 paper_attachment 表里维护了一个修订轮次字段,统计平均审稿轮次时,直接按这个字段分组聚合,非常方便。
有个细节值得一说:有些系统会把返修的截止日期做成硬限制,到期未提交就自动拒稿。我的做法是软提醒而非硬限制,到期前三天提醒作者,到期后不自动拒稿,允许作者联系编辑申请延期。学术场景里,硬自动操作往往过于粗暴,给人工留一个干预入口更合理。
5. 定时任务与消息提醒的实践
5.1 定时任务的选型与实现
消息提醒是提升系统体验的关键。但这里说的提醒,并不仅仅是"弹个通知",而是把"该发生的动作"按时间线管理起来。
我首选 Spring 自带的 @Scheduled 注解来实现定时任务,没有引入 Quartz 或 XXL-Job。原因很简单:系统的任务类型只有三种,且都是固定周期执行,单机部署下 @Scheduled 完全够用。引入重型的分布式调度框架反而增加了运维负担。
三种定时任务分别是:
- 审稿截止提醒:每小时执行一次,查出所有"截止时间在 24 小时之内且尚未提交意见"的审稿分配,给审稿人发送通知;
- 投稿超时未处理提醒:每天一次,对超过三天仍未处理的新投稿,提醒编辑尽快处理;
- 数据统计缓存:每天凌晨统计一次投稿量和录用率,结果存入缓存表,供管理端展示,避免每次请求都实时聚合大表。
5.2 消息提醒的多通道设计
技术方案上,最方便的是引入 WebSocket 做在线通知,但这套系统我并没有用 WebSocket,而是用了一个更朴素的方案:站内信 + 邮件。
- 站内信:在数据库中建了一张通知表,用户登录后在右上角红点查看未读消息;
- 邮件:关键节点(如审稿邀请、录用通知)同时发送邮件,通过 Spring Mail 接入 SMTP 服务。
选这个组合的原因是投稿系统的用户"在线时长"并不固定。作者和审稿人不会全天候挂在系统里,WebSocket 实时推送的收益有限。邮件能触达手机,站内信能留存为操作记录,两者结合已经非常完整。等用户体量上来了,再考虑接入企业微信或钉钉机器人也是水到渠成的事。
6. 常见问题与排查技巧实录
6.1 文件上传超时
上线初期收到最多的问题就是"传大文件转圈"。排查发现,Nginx 默认对请求体大小没有限制,但代理转发时有 60 秒的超时设置,文件稍微大一点,后端还在写磁盘,Nginx 已经断开连接了。
解决方法是同时调整 Nginx 的 client_max_body_size 和后端 Spring Boot 的 spring.servlet.multipart.max-file-size,把两者都设置到统一的上限(我设置的是 50MB),Nginx 的超时时间也适当放宽到 300 秒。这里的关键教训是:链路里每一个环节的配置都要对齐,前端限了、后端限了,但 Nginx 没放开,照样出问题。
6.2 审稿人不小心提交了空意见
有个现实场景:审稿人在表单里填了很多内容,但点提交时因为某个字段校验不通过,页面刷新后发现之前填的内容全丢了。这不是 bug,但用户体验非常差。
我后来做了一个自动保存草稿的功能:审稿人在表单输入时,前端每 30 秒自动把当前内容存到浏览器 localStorage,手动提交成功后才清除。如果提交失败或页面意外关闭,再打开时自动恢复。这是个小功能,但对审稿人来说非常友好,上线后好评不少。
6.3 超时并发导致状态错乱
测试阶段发现一个隐蔽的 bug:编辑对同一篇论文同时点了"进入审稿"和"拒稿"两个按钮,由于两个请求并发到达后端,都通过了状态校验,导致数据库里最终状态取决于谁后执行,留下了逻辑漏洞。
后来在状态机的方法上加了 synchronized,并且统一由状态机方法负责写入状态和日志,业务层不再直接修改 paper 的 status 字段。单机部署下这个问题就彻底解决了。如果你是多实例部署,记得把这里的锁方案换成 Redis 分布式锁,或者在 SQL 层面加上"乐观锁版本号"。
6.4 审稿意见提交后无法修改
原设计里,审稿意见一旦提交就不可更改,本意是保证审稿的独立性。结果上线后编辑提了个需求:审稿人误操作提交了空意见,或者发现了明显的错误需要更正,系统却没有入口。
我的做法不是开放"自由修改",而是增加"补充意见"的入口:审稿人可以再次提交一条补充意见,原意见保留,新意见追加,系统在最终结论里同时展示。这样既保留了痕迹,又给人工纠错留了空间。这也算是业务规则与技术实现的折中。
6.5 常见问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 登录后很快掉线 | JWT 过期时间过短 | 将有效期调整为 24 小时,前端增加自动续期逻辑 |
| 论文文件下载失败 | 文件路径变更或文件被误删 | 文件路径存相对路径,服务启动时校验目录是否存在 |
| 审稿分配后审稿人看不到 | 通知未生成或状态不对 | 检查通知表记录;确认 assignment 状态是否为"待审" |
| 邮件发送失败 | SMTP 账号密码错误或频率限制 | 查看邮件异常日志;检查 SMTP 服务的每日发信额度 |
| 统计数字与人工台账对不上 | 时间范围口径不一致 | 统一按状态日志的变更时间统计,并在页面标注统计口径 |
7. 权限与安全:最容易忽略的细节
7.1 接口层面的细粒度控制
前面提到 @PreAuthorize 做接口级控制,但在复杂场景里还需要配合数据级校验。比如作者访问 /papers/{id} 获取详情,普通的接口鉴权只能保证"登录用户"能访问,不能保证"作者本人的文章才可访问"。
我的做法是在论文 Service 中增加一个通用方法:
java复制private void checkPaperAccess(Paper paper, User currentUser) {
if (currentUser.isAdmin()) return;
if (currentUser.isEditor()) return;
if (paper.getAuthorId().equals(currentUser.getId())) return;
throw new AccessDeniedException("无权访问该论文");
}
所有涉及论文详情的接口,第一件事就是调这个方法,保证不会越权访问。这种"接口级 + 数据级"的双层校验模型,是整个系统权限安全性的基石。
7.2 操作日志与审计追踪
因为系统涉及学术评价的敏感数据,审计功能是刚需。我用 AOP 切面统一实现了操作日志记录:任何对论文状态的修改、任何审稿意见的提交,都会自动记录操作人、操作时间、操作内容和操作前后的数据快照。
这个功能的价值平时看不出来,一旦出现"作者说我没投过这篇稿子"或者"审稿人说我没提交过意见"之类的纠纷,操作日志就是唯一的裁判依据。实现上用 Spring AOP 的 @Around 注解,圈定需要记录的方法,解析用户上下文后写入日志表,跟业务代码零侵入。
7.3 敏感操作的二次确认
涉及到拒稿、删稿这种不可逆操作,我都要求前端弹出二次确认框,后端在接口层再加一个参数开关(例如 confirm=true,否则直接拒绝请求)。这个看起来是"多此一举",但实际运行中至少拦截了十几次误操作。
8. 回顾与扩展方向
这套 m231论文投稿系统从立项到基本可用,前后大约用了两个月时间,大部分时间花在需求梳理和状态流转的逻辑设计上。回头来看,最有价值的决定有两个:一是状态流转集中管理,守住了数据一致性的底线;二是版本管理独立成表,让"返修了几轮"这件事变得一清二楚。
如果你也在规划类似的流程系统,我建议先花足够的时间把状态流转图画清楚,画图时不要只画主线,一定把各种"驳回""撤回""退稿"的分支全部画出来,再去写代码。状态流转图画清楚了,后面的代码就是体力活。
这个系统后续有几个可以扩展的方向:接入全文查重服务,在投稿环节自动触发重复率检测;增加多维度的统计报表,按学科方向、投稿周期等维度交叉分析;或者把审稿意见的结构化程度再提高一些,为未来做审稿质量分析积累数据。根据团队的实际需求,按需推进就好。
最后说一点个人体会:做系统最怕的不是技术难点,而是把简单流程做得复杂。投稿本质就是"投上去、有人看、给结论"三件事,围绕这个核心做系统,把所有额外功能当作可选项而非必需项,节奏就稳了。
