
一个论文投稿系统听起来不就是个表单加数据库嘛真做起来琐碎程度远超预期。从作者注册、稿件上传、格式校验到编辑分配审稿人、审稿意见回传、录用退修状态流转再到后期检索、统计每个环节都是一堆状态要维护、一堆权限要控制。我这次用SSM框架做核心业务后端前端配合分段渲染再把Flask作为辅助服务嵌进来处理文本相似度和关键词匹配把整个投稿闭环完整跑通了。这篇就把我的设计思路、数据库建模、核心代码片段、以及调试阶段踩过的那些坑一起梳理清楚给准备做课设、毕设或者想快速搭一个内部投稿平台的同行做个参考。这个项目适合两类人看一类是正在做JavaWeb课设或毕设的学生另一类是真的需要在教研室、课题组内部搭一套轻量投稿与审稿流程的技术人员。前者可以把它当作SSM框架整合的完整案例后者可以直接抄作业——里面的期刊配置、稿件状态机、审稿分配策略都是可以直接落地的设计。1. 项目整体设计与技术选型思路1.1 为什么是SSM加Flask的混合架构先说技术选型。标题里面写的是Java加SSM加Flask很多第一次看到这个组合的人会疑惑SSM一套就能做完的事为什么还要引入Flask这不是给自己找麻烦吗老实说如果只做增删改查SSM确实足够了。但论文投稿系统里有两个功能用Java做起来很别扭一是稿件关键词的语义匹配与相似度计算二是基于文本内容的自动分类与查重预检。Java写余弦相似度、Jaccard相似度本身不难难的是中文分词和停用词处理这一整套文本预处理链路。Jieba分词、Word2Vec、TF-IDF向量化这些东西Python的生态比Java成熟太多十几行代码就能搞定一个效果还不错的中文文本相似度服务。所以我的设计思路是核心业务用SSM管——用户体系、权限控制、投稿流程、审稿流程、期刊管理、数据统计这是SSM的舒适区三层架构清楚明白事务好控制而文本分析、关键词智能匹配、参考文献格式预检这些小而杂的功能拆出去做成Flask的独立服务对外暴露HTTP接口由SSM在业务层去调用。这个拆法有一个额外的好处两个模块可以独立部署、独立扩展。系统流量小的时候一起跑在同一台机器上甚至不同端口即可以后量大了Flask服务可以单独拆到另一台服务器上SSM这边只需要改一个服务地址的配置项。这种核心业务重架构、辅助功能轻服务的思路生产环境里非常常用放在课设和毕设里也是一个很好的加分点。1.2 SSM框架演进中的兼容处理现在纯说SSM其实有歧义。早期的SSM是Spring SpringMVC MyBatis工程里要写一堆XML配置数据源、事务、扫描器、视图解析器全手动装配每次搭环境都得折腾一阵。而现在大多数学校虽然还写着SSM框架实际上已经开始用Spring Boot去整合SpringMVC和MyBatis了也就是Spring Boot MyBatis的变体只是习惯上仍沿用SSM的说法。我项目里采用的是兼容路线核心代码按Spring Boot 2.7 MyBatis的结构来写但包分层严格遵守Controller / Service / Mapper / Entity的传统分层风格这样无论你的导师要求看SSM的配置方式还是Spring Boot的自动装配方式都能拿出对应的说明。如果一定要用传统SSM的XML配置版本改造成本也不高把Spring Boot的自动配置换成applicationContext.xml和spring-mvc.xml即可Service层的代码完全不用动。这里有个经验做课设或毕设前先跟导师确认清楚允许的技术版本。遇到过不少学生按Spring Boot写了三个月答辩前导师说这个不是SSM临时推倒重来。实际上Spring Boot整合的SpringMVC和MyBatis本质就是SSM框架的现代化形态你只需要在文档里把这个演进逻辑解释清楚就行。2. 核心功能模块拆解与数据库设计2.1 三类角色与业务流程闭环投稿系统不是简单的作者传个PDF上去就完事的。一个完整的投稿业务至少要覆盖三个角色作者端注册登录、维护个人信息、在线投稿填标题、选期刊栏目、写关键词、上传稿件文件、查看自己稿件的审稿进度、收到退修意见后上传修改稿、查看录用通知。编辑/管理员端管理期刊栏目信息、初审稿件查重结果、格式检查、分配审稿人、根据审稿结论做最终决定录用/退修/退稿、发布录用公告。审稿人端查看分配给自己的待审稿件、下载稿件文件、在线填写审稿意见、给出结论建议推荐录用/修改后录用/退稿。这三个角色的权限边界要清晰。作者只能看到自己的稿件审稿人只能看到分配给自己的稿件编辑能看到当期所有稿件但本身不参与评审打分。我采用的是RBAC基于角色的访问控制用户表里存role字段Shiro或Spring Security做拦截器校验。这个项目里为了简化我用了Spring Boot拦截器自定义注解的方式三个角色各对应一个拦截规则比引入完整安全框架更轻量代码也更直观。业务流程上有几个状态是关键节点。作者的投稿操作提交后稿件状态是待初审编辑初审通过后变成外审中同时系统自动给分配到的审稿人发通知审稿人提交意见后稿件变成待终审编辑根据意见做出决定稿件进入已录用/退修/已退稿中的某一个终态如果是退修作者上传修改稿后重新进入审稿循环。2.2 数据库表设计中的关键权衡数据库是整个系统最值得花时间设计的部分表建得合理后面写SQL和Service层都顺表建得随意后面每个功能都在为当初的偷懒买单。我的核心表有七张用户表、角色表、期刊栏目表、稿件表、稿件附件表、审稿记录表、系统日志表。其中稿件表是最复杂的它的字段设计直接决定业务的表达能力。稿件表里除了基础的标题、摘要、关键词、作者ID、投稿时间我还要特别说三个字段第一是current_status存当前状态码同时配合status_history字段记录完整流转历史。状态码我用的整数枚举0-待初审、1-外审中、2-退修、3-已录用、4-已退稿。为什么不直接用字符串存状态名称因为状态在代码里是要参与逻辑判断的枚举值比字符串可以避免外审中外审中 这类带空格或命名不一致的脏数据。第二是journal_id关联期刊栏目表。投稿系统一般都支持多个期刊或同一个期刊的多个栏目提前把这个抽象出来以后扩展栏目、按栏目统计稿量都方便。第三是similarity_report字段存查重预检报告的路径。Flask服务算完相似度之后返回的不是一个简单的百分比而是一份JSON报告里面包含最相似的前五篇文章标题和对应相似度。这个报告我以文件形式存储数据库里只存路径避免把大文本塞进业务表拖慢查询。附件表需要单独建的原因有两个一篇投稿稿件在流转过程中会有多个版本——原始稿、修改稿、最终定稿另外审稿人下载时也要能追溯到具体是哪个版本。所以附件表里加了一个version字段和file_type字段每次上传都生成一条附件记录关联到稿件主键ID。审稿记录表的设计也有讲究。一个稿件在退修循环里可能被多个审稿人审过一个审稿人也可能审同一篇稿件的不同版本。所以审稿记录表的主键我用的自增ID关联字段有稿件ID、审稿人ID、审稿轮次、意见内容、结论建议、审稿时间。查询某个稿件的最新审稿记录时按稿件ID和审稿轮次倒序取第一条即可逻辑很清爽。2.3 稿件状态机的显式管理状态流转这件事很多人是写在Service层的一堆if-else里。比如投稿要判断稿件当前是不是待初审状态才能进入外审写法是从数据库查状态、判断、更新。这样写本身没错但当状态越来越多时散落的if-else很容易漏掉某个分支而且后来者读代码时很难快速把握整个流程。我在项目里做了一张状态机配置表把每一个状态迁移需要的触发动作、前置状态、目标状态、权限角色都配置化。代码里统一走一个transition(manuscriptId, targetStatus, operatorId)的方法方法内部通过配置校验这次迁移是否合法。这样有两个直接好处一是状态流转图可以从配置表里直接生成写文档画图不用愁二是非法操作比如作者想把待初审直接改成已录用会被配置校验直接拦截从根上杜绝了越权状态变更。这段配置逻辑配下来业务上就稳了一大半。后面所有涉及稿件状态变更的功能都强制走这一个方法不单独在业务代码里直接改状态字段。3. 实操从零搭一个可运行的投稿闭环3.1 环境准备与工程结构开发环境这块我给一个能稳定复现的组合JDK 1.8或17都可以建议新项目直接17但MyBatis和依赖版本注意要兼容Jakarta命名空间的问题Maven 3.6以上MySQL 5.7或8.0Python 3.8以上配Flask 2.x。工程结构上我采用Maven多模块的拆分思路。父工程下分ssm-core放Java后端主程序flask-service放Python辅助服务。如果你只用单个Maven工程也可以按包名区分——把调用Flask的客户端类放在com.example.submit.service.integration包里保持层次。Java后端的主包结构我这样组织com.example.submit ├── controller # 控制层接收前端请求 │ ├── AuthorController # 作者端接口 │ ├── EditorController # 编辑端接口 │ └── ReviewerController # 审稿人端接口 ├── service # 业务逻辑层 │ ├── ManuscriptService # 稿件核心业务 │ ├── UserService # 用户与权限 │ └── ReviewService # 审稿业务 ├── mapper # MyBatis数据访问层 ├── entity # 数据库实体 ├── config # 配置类 │ ├── InterceptorConfig # 登录拦截 │ └── RestTemplateConfig # 调用Flask的客户端配置 ├── common # 工具类、统一返回体、异常处理 └── dto # 前端交互的数据对象Flask服务那边结构简单一些flask-service ├── app.py # Flask应用入口 ├── analysis/ │ ├── similarity.py # 相似度计算 │ ├── keyword_extract.py # 关键词提取 │ └── preprocess.py # 中文分词与停用词过滤 ├── static/ # 静态报告目录 └── requirements.txt3.2 核心链路投稿与文件上传的实现投稿是系统的第一个核心操作它的完整流程是前端表单提交元数据标题、摘要、关键词、栏目ID同时以multipart/form-data格式上传稿件文件后端Controller接收后做参数校验文件存储再开事务写入稿件表和附件表。文件这块有几个容易踩坑的点我先给结论。第一上传目录不要直接放在项目打包后的target或static里否则每次重新打包部署文件就丢了。我在配置项里设置了一个绝对路径upload.dir默认指向服务器上的/data/submit/files这种目录Windows上就配成D:/submit/files。第二文件名不要用原始文件名入库要加UUID前缀重命名防止两个作者传了同名文件互相覆盖。第三数据库里存相对路径比如2025/04/abc-123.pdf完整的访问地址由配置拼出来以后迁移存储位置时不用改数据库。下面这段是投稿接口的核心代码逻辑PostMapping(/api/manuscript/submit) public Result submit(Valid RequestBody ManuscriptDTO dto, RequestParam(file) MultipartFile file) { // 1. 校验投稿人身份 UserVO user UserContext.getCurrentUser(); if (!RoleConstants.AUTHOR.equals(user.getRole())) { return Result.fail(权限不足); } // 2. 存储稿件文件 String filePath fileStorage.store(file, manuscript); // 3. 构建稿件实体并写入数据库 Manuscript manuscript new Manuscript(); BeanUtils.copyProperties(dto, manuscript); manuscript.setAuthorId(user.getId()); manuscript.setCurrentStatus(StatusEnum.PENDING_FIRST_REVIEW.getCode()); manuscriptService.submit(manuscript, filePath); // 4. 同步调用Flask服务做关键词匹配异步生成查重报告 integrationService.asyncAnalyze(manuscript.getId(), dto.getKeywords()); return Result.ok(); }这里有个细节第2步文件的存储和第3步数据库写入严格来说不是一个原子操作。如果文件存了但数据库事务回滚磁盘上会残留孤儿文件。我现在的处理是文件存储成功后如果数据库写入失败在catch块里补一个删除文件的补偿操作。这种先做后补的方式比较复杂但课设里把补偿逻辑写在博客里讲清楚是很加分的。更优雅的做法是先写数据库再存文件那你得把文件临时存到临时目录数据库提交成功后再移动到正式目录逻辑更绕但一致性更好。我实际用的是先存后删补偿简单直接。3.3 映射层与XML编写要点MyBatis这块我推荐用XML写SQL而不是全用注解。原因很简单投稿系统里的查询条件组合多变比如管理端的稿件列表要按标题模糊搜索、按栏目筛选、按状态筛选、按时间范围筛选、按作者名筛选动态SQL用XML的where加if标签组合最方便注解拼接不是不能写但可读性差很多。一个典型的动态查询示例select idpageQuery resultTypecom.example.submit.entity.Manuscript SELECT m.*, u.username AS author_name, j.journal_name FROM manuscript m LEFT JOIN user u ON m.author_id u.id LEFT JOIN journal_column j ON m.journal_id j.id where if testtitle ! null and title ! AND m.title LIKE CONCAT(%, #{title}, %) /if if teststatus ! null AND m.current_status #{status} /if if testcolumnId ! null AND m.journal_id #{columnId} /if if testauthorName ! null and authorName ! AND u.username LIKE CONCAT(%, #{authorName}, %) /if /where ORDER BY m.create_time DESC /select这里有个MyBatis的老坑实体属性是驼峰命名数据库字段是下划线命名比如currentStatus对应current_status如果没开启驼峰映射查出来都是null。在Spring Boot的application.yml里一行配置解决mybatis: configuration: map-underscore-to-camel-case: true如果用的是传统SSM的XML配置方式对应的是settings里的mapUnderscoreToCamelCase属性。这个配置不加上早期的数据查询结果会充满各种null字段排查起来很迷惑。分页我用的PageHelper依赖引入后直接用PageHelper.startPage(pageNum, pageSize)紧接着的第一条SQL会被自动拼接limit语句查询后拿PageInfo就能得到总数、总页数这些分页参数。注意PageHelper只对紧跟着的那一条查询生效如果你在startPage和查询之间插了别的SQL操作分页就会失效甚至报错这是使用频率最高的一个坑。3.4 Flask端相似度匹配服务的接入Flask服务在这个系统里的职责是稿件关键词匹配与相似分析。场景是这样的作者投稿时填写的关键词系统要在他投的期刊栏目里检索历史稿件计算新稿件与历史稿件在关键词和标题上的相似度把最相近的几篇列出来作为查重参考。这里的关键词匹配核心不是简单的字符串相等判断而是要做语义层面的近似匹配。比如作者投稿的关键词是机器学习历史稿件里有一篇写的是深度学习字符串完全不同但语义上是强相关。这就是Flask服务的价值所在。我实现的匹配流程分三步。第一步用jieba分词把关键词做切分同时过滤掉停用词。第二步用TF-IDF向量化把关键词和候选稿件的标题分别转成向量形式。第三步计算余弦相似度。核心代码并不复杂Python写这种文本处理任务确实是降维打击import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def calc_similarity(keyword_str, candidate_titles): # 关键词与候选标题组成语料 corpus [keyword_str] candidate_titles # 自定义分词器过滤单字和停用词 def tokenizer(text): words jieba.lcut(text) stopwords {的, 了, 与, 和, 基于, 一种, 研究, 方法} return [w for w in words if w not in stopwords and len(w) 1] vectorizer TfidfVectorizer(tokenizertokenizer) tfidf_matrix vectorizer.fit_transform(corpus) sims cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:]).flatten() return sims.tolist()Flask服务对外只暴露一个接口POST /api/analyze/similarity接收稿件ID列表和关键词返回每篇的相似度分数。Java这边用一个简单的RestTemplate客户端做HTTP调用。Flask服务接入的整体效果是投稿后两三秒内会生成一份查重预览报告编辑初审时直接看到系统标红的相似稿件效率提升非常明显。3.5 部署与服务间通信细节部署方式我推荐最省事的两进程方案Java后端打jar包Java服务跑8080端口Flask服务用app.run(port5000)起在5000端口。两边都在同一台服务器上时Java的Feign或RestTemplate直接配置http://localhost:5000即可。关键是要解决跨域问题。前端页面如果单独跑在Nginx上比如http://your.server:8088它同时要访问后端的8080和Flask的5000端口浏览器会有跨域限制。我的处理分两头解决Java后端用CorsFilter统一放开跨域Flask端用flask-cors扩展这是一个很好用的辅助手段from flask_cors import CORS app Flask(__name__) CORS(app, resources{r/api/*: {origins: *}})这里要提醒一个安全上的细节跨域设置origins: *在开发阶段没问题但如果系统要放到公网环境建议收敛成具体的前端域名避免任意网站都能调用你的投稿接口。课设阶段可能没人攻击你的项目但习惯要养好。服务间通信还有一个时序问题投稿接口是同步返回的业务不能等Flask的文本分析跑完才给作者响应。不然作者传完稿子要等好几秒才能看到成功提示体验很糟糕。我的做法是用Spring的Async注解把Flask调用异步化投稿主流程快速返回文本分析的结果通过后台线程写入数据库。体验最优。4. 常见问题与调试记录4.1 高频问题速查表整理一下这个项目里最常遇见的几类问题有环境的有代码的有部署的问题表现根因解决方案数据库查询结果某些字段为null没有开启MyBatis驼峰映射配置map-underscore-to-camel-case: true上传文件丢失或404上传目录配置在classpath内使用外部绝对路径如/data/submit/files中文乱码前后端编码不一致统一UTF-8CharacterEncodingFilter并设置forceEncoding分页不生效或SQL错乱PageHelper位置不对startPage必须紧跟目标查询SQLFlask接口Java调用超时RestTemplate没有配置连接超时设置connectTimeout和readTimeout端口被占用导致启动失败残留进程或端口冲突管理端换端口或排查占用进程作者上传的文件别人能下载没有做权限校验下载接口校验登录角色和关联关系4.2 三个值得展开的高频坑第一个坑是上传文件的路径处理。Windows环境下如果不小心把File.separator写死成反斜杠代码到了Linux服务器上会全部失效。我自己在项目里统一用的是存储时把路径标准化成相对路径存数据库用/分隔文件访问时在Controller层拼装完整路径同样用/。Java的Paths.get(upload.dir, relativePath).toString()会自动适配操作系统分隔符比手动拼接字符串省心太多。第二个坑是Spring Boot 2.7和较低版本MyBatis的兼容性。Spring Boot 2.4之后spring.factories这个自动配置加载机制对第三方starter不再友好MyBatis官方的mybatis-spring-boot-starter如果不升到2.1.3以上版本会出现在启动时SPI读取不到配置的诡异报错。这个报错信息很不直观解决方式就是Spring Boot 2.7对应MyBatis starter 2.2.x及以上Spring Boot 3.x要对应MyBatis starter 3.0.x。版本锁定这个事最好在pom.xml里统一用dependencyManagement做约束。第三个坑是审稿并发更新。两个审稿人同时审同一篇稿件都提交了审稿意见后提交的如果不做校验可能把前面审稿人的结论覆盖掉。我的解决思路是在更新审稿记录时SQL里带上前置状态条件比如UPDATE manuscript SET current_status #{target} WHERE id #{id} AND current_status #{expect}影响行数为0就说明状态已被别人改过提示稿件状态已变化请刷新后重试。这种乐观锁的思想在论文系统的并发场景里应用起来非常合适比引入分布式锁轻量得多。4.3 调试过程中的效率技巧调试前后端联调接口时我习惯把每个接口的参数和返回结果都打日志。Spring Boot里用AOP切面统一打印请求参数和响应耗时这样排查问题不用一个个装POSTMAN重新调翻了日志就能定位大部分问题。Flask端的调试相对Java稍微痛苦一点因为它报错信息有时候不太直观。我调试相似度服务时先把入参和分词结果打印出来确认分词这一步是否符合预期。很多相似度结果是0都是分词阶段出了问题——要么全部被停用词过滤要么没有命中候选语料。分词结果正常再往向量化那一步排查。5. 代码之外文档写作与答辩演示的经验5.1 LW论文文档写作的结构把握这类带源码的项目配套文档占据的比重很大。开头提到的LW通常指的是论文或设计说明书。文档写作我有一个强烈建议不要按代码模块顺序写要按业务需求顺序写。很多同学喜欢从数据库连接工具类封装这种技术细节开始写结果文档读起来像代码注释的堆叠。正确的组织结构应该是需求分析部分把三类角色的业务痛点列清楚每个痛点对应一个功能用例对应关系用表格画出来。系统设计部分先给一张整体架构图然后是功能模块划分、数据库ER图、核心表字段说明。系统实现部分不需要把代码全贴出来而是挑有代表性的核心逻辑比如状态机控制、文件上传的异常处理、Flask相似度计算用需求背景-实现思路-核心代码-效果截图四段式展开。测试部分至少要有功能测试用例表格和几类异常场景测试记录包括上传空文件、重复投稿、越权访问等。文档这块还要注意一个细节数据库设计的章节里列出每个字段时一定要说明它的业务含义特别是status这类状态字段要附上状态码枚举说明表。评审老师拿到文档最先翻的就是数据库设计字段含义不清是最常见的扣分项。5.2 演示前必须跑通的全流程路径答辩演示环节系统功能点多但演示时间通常有限所以要设计一条最能展示系统能力的演示主线。我建议按这个顺序演示作者注册登录、在线投稿并上传文件、投稿成功后稿件查重报告自动生成、编辑登录初审并通过、系统自动分配审稿人、审稿人提交意见、编辑终审录用、作者端看到录用通知。这条路径覆盖了系统全部核心功能而且有明确的故事线。演示前有几个地方要提前检查。第一服务器上先把要用的样例账号、样例稿件准备好不要在演示现场现注册。第二把稿件上传目录清空或准备好防止磁盘空间不足的问题。第三Flask服务要确保已经启动我见过太多演示现场Java后端起来了Flask挂了查重报告转半天出不来流程卡死。建议在启动脚本里把两个服务的健康检查都做上启动时自动弹出一个检查报告。还有一个容易被忽略的点演示用的数据应该让人一眼看出效果。比如投稿相似度匹配如果你上传一篇标题和关键词都与历史上某篇文章高度相似的稿件系统立刻标红显示相似度87%这个视觉冲击力远比用一个全新主题的稿件强得多。演示数据的准备也要花心思。6. 项目经验复盘与后续扩展方向最后从我个人角度说几点体会。整套系统从需求梳理到最后跑通最有价值的不是某一个技术点而是业务流程和技术实现的互相映射这个思维。稿件状态机设计好坏直接决定审稿流程的稳定程度文件存储路径的一念之差决定了后续部署维护省心还是闹心Flask和SSM的服务边界怎么划决定了开发效率是11大于2还是两个模块互相拖累。做这种完整系统技术选型永远排在写代码前面架构设计想清楚了后面的编码基本都是体力活。如果后续扩展我建议优先做这几个方向一是接入更成熟的全文检索引擎替代现在的SQL模糊查询比如Elasticsearch能大幅提升稿件检索效率二是把审稿意见的结构化让系统能自动汇总多份审稿意见生成退修通知三是加一个邮件或站内信的提醒中心投稿状态每变更一次就自动通知相关角色。论文投稿系统这类项目做成什么样才算好我的评判标准是业务流程完整闭环状态流转严谨无漏洞文件数据不丢不串角色权限清晰不越界。这些基础都做到位了技术上哪怕朴素一点也是一个合格甚至优秀的系统。技术花活只是锦上添花业务的严密才是投稿系统的灵魂。