ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot小型船舶进出港登记系统设计与实现

SpringBoot小型船舶进出港登记系统设计与实现 springboot小型船舶进出港登记系统一眼看过去像是从毕业设计题海里随手捞出来的常规题目但真把这套系统从头做下来你会发现它比图书管理、考勤打卡这类“烂大街”题目更容易做出业务深度也更好写论文。只要你把进出港的业务规则、状态流转、审核链路理解透这套系统可以做到既有技术含量又有行业辨识度。这篇文章我按自己做这类管理系统的完整思路来讲从业务建模、技术选型、表结构设计到后端落地、踩坑排查最后再加几个答辩加分的设计点。不管你是刚开题还是已经写了半截代码应该都能从中找到值得抄走的东西。1. 小型船舶进出港登记系统这个选题到底在解决什么业务问题1.1 业务场景与用户角色先说清楚“小型船舶进出港登记”是个什么场景。沿海沿江的港口、渔港、内河码头每天都有大量小型运输船、渔船、旅游客船进出。这些船不能随便靠岸离岸港务或海事相关的登记管理人员需要记录它们的到港时间、离港时间、船名船号、船员信息、载货情况、目的港等。纸质登记时代就是一本台账船老大到窗口报一声值班人员手写一条一天下来一叠纸。问题是查询费劲、统计靠翻、容易漏记也无法快速判断一条船是否已经完成出港手续、能不能再次申报。这套系统的核心任务就是把“进出港登记”这件事从纸质账本搬到线上顺便解决掉台账时代留下的三座大山查询难、统计难、追溯难。我建议把用户角色划分成三类别贪多角色职责边界船方用户注册船舶档案提交进出港申请查看审核结果港口登记审核员审核进出港申报驳回或通过完成后签证记录系统管理员维护船舶档案、用户权限、数据字典查看统计报表这个划分最贴近真实业务也方便你在论文里画用例图。让船方自己申报、审核员后台处理比所有操作都堆在一个管理员账号上要合理得多。1.2 把进出港流程抽象成状态流转在做需求分析时最容易犯的错就是直接开写CRUD结果做完发现不过是给数据库做了一套增删改查外壳。正确姿势是先画业务链路再Translate成状态机。小型船舶进出港核心流程是两条一条是出港流程。船方提交出港申请填清目的港、预计离港时间、随船人员、货物信息审核员核验船舶证书和费用缴清情况通过后生成出港签证记录船舶离港船舶在港状态变为“在航”。另一条是进港流程。船舶到达后提交进港申请登记实际到港时间、上一港、航次货物摘要审核员核对船名船号属实记录进港船舶状态回到“在港”。两条流程在状态上是闭环的船必须在“在港”状态才能申请出港出港完成后它才允许再次进港进港完成后才允许下一次出港。这实际上是一个简单的状态机你把它理顺了后面的代码和论文都好写。1.3 容易被忽略的“隐藏需求”很多照着模板做毕设的同学会把需求分析写成功能清单但答辩时老师问一句“为什么需要这个功能”就接不上话。我建议在需求文档里至少写上这三条隐藏需求第一重复申报的拦截。一条船不能在未完成出港核验时反复提交出港申请系统需要根据当前船舶的在港状态做前置校验。第二登记记录的可追溯性。任何一条记录被审核、被驳回、被修改都要留下痕迹这一条直接对应操作日志。第三进出港量的统计分析。港务调度需要知道一天进出多少条船、船型分布是什么这对应统计报表模块。这三条每个都是在真实场景里会被追问的点提前做进去论文里就能有完整的“需求分析—功能设计—系统实现”对应关系。2. 技术栈选择与版本控制SpringBoot为主干谁能当它的最佳搭档2.1 为什么这套系统选SpringBoot而不是SSH或SSMSSH和SSM在五六年前还是主流现在再拿Struts或者Spring MVC手写XML配置做毕设不是不行而是没必要。SpringBoot的自动装配机制把大量样板配置收进了内部你只需要关心业务代码。做一个中小型管理系统SpringBoot构建的REST接口加上前端页面工程量和可控性都比SSM时代好太多。更重要的是SpringBoot天然适合做单体系统。小型船舶进出港登记系统这种体量用户的并发量不高业务逻辑集中在登记、审核、状态流转上单体架构就是最简单可靠的方案。强行引入微服务或者分布式中间件只会给自己挖坑。2.2 SpringBoot版本选型的现实问题这里必须多说一句因为我看到太多人在版本上栽跟头。SpringBoot 2.x和3.x的差异不只是大版本号不同3.x要求JDK17并且底层用的是Jakarta EE命名空间有些旧教程里的代码直接搬过来会报错。如果你的重点是尽快把业务做出来我的建议是选SpringBoot 2.7.x JDK8这套组合稳定、资料多、踩坑少。拿Maven依赖来说核心配置写干净就行parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies别小看版本这一行MyBatis Plus的版本和SpringBoot之间偶尔会有兼容性问题选一个被广泛使用的搭配网上能搜到的问题解决方案也最多。2.3 前端和数据库怎么搭配更省事前端我建议用Vue3 Element Plus Vite。Vite的启动速度和热更新体验比Webpack好Element Plus的表格、表单、弹窗组件正好覆盖登记管理的全部界面需求。如果你不是专门搞前端的这一套能让你把精力留在后端业务上。数据库用MySQL 8.0存储引擎InnoDB字符集用utf8mb4。表结构设计得当的话整个系统完全不需要存储过程或者复杂视图业务逻辑放在Service层就够了。3. 表结构设计船舶档案、进出港记录、状态机与冗余字段的取舍3.1 核心表总体规划系统围绕船舶档案和进出港记录两条主线建表。我建议至少规划这几张表表名用途关键字段sys_user系统用户id, username, password, role, statusship_info船舶档案id, ship_name, ship_no, ship_type, tonnage, owner_name, owner_phone, captain_name, statusentry_exit_record进出港登记记录表id, ship_id, record_type, declare_time, actual_time, target_port, cargo_info, crew_count, audit_status, visa_time, remarkaudit_log审核操作日志id, record_id, operator_id, action_type, content, create_timesys_dict数据字典id, dict_type, dict_label, dict_value核心业务表就这五张足够支撑起整个系统也能让你在论文里把ER图画清楚。3.2 船舶档案表的几个设计细节船舶档案表的Ship表在设计时最容易忽略的是唯一性约束。ship_no是船舶登记号一个系统里绝对不能出现两条一样的船号所以要建唯一索引。这里有个细节后面会专门讲到如果你做了逻辑删除字段deleted唯一索引的选择就得额外小心。我建议船舶档案表加上状态字段status用来标识这条船是“正常”“停用”还是“已注销”。因为进出港登记必须先校验船只是否处于可航行状态状态字段可以在Service层直接判断不需要关联查询其他表。船舶档案里我还建议冗余一个在港状态字段current_berth_status表示这条船当前是“在港”还是“在航”。这个字段看着像冗余实际非常有用。否则你要判断一条船能不能申请出港得去进出港记录表里查最新的未完成记录多一次查询不说逻辑也绕。表空间先简单搭成这样TableName(ship_info) public class ShipInfo { TableId(type IdType.ASSIGN_ID) private Long id; private String shipName; private String shipNo; private String shipType; private BigDecimal tonnage; private String ownerName; private String ownerPhone; private String captainName; private Integer currentBerthStatus; // 0在港 1在航 private Integer status; // 0正常 1停用 TableLogic private Integer deleted; }3.3 进出港登记表用record_type区分进出用audit_status统一状态流进出港登记是核心业务这张表的字段设计要考虑两件事一是同一张表能不能同时存进港和出港记录二是审批状态如何统一表达。我建议用一张entry_exit_record表通过record_type字段区分0代表进港1代表出港。这样做的好处是统计进出港量时一条SQL就能完成不需要join两张表。审批状态用audit_status统一表示0待审核、1已通过、2已拒绝、3已签证。整个表的状态机如下当前状态允许操作目标状态待审核审核通过 / 审核驳回已通过 / 已拒绝已通过签证确认已签证已拒绝重新编辑申请待审核已签证无操作终态无这张表同时记录申报时间和实际时间declare_time是船方提交申报的时间actual_time是审核通过后实际离港或到港的时间。这个设计在业务上是合理的因为申报和实际动作之间存在时间差论文里也能体现出你对业务流程的思考。4. 后端接口落地从进出港申报到审核签证的完整实现链路4.1 接口清单与统一返回结构后端接口不需要搞得太花哨但要完整覆盖业务。我整理的接口清单如下POST /api/entryExit/apply船方提交进出港申报PUT /api/entryExit/{id}/audit审核员审核申报通过或驳回PUT /api/entryExit/{id}/visa出港签证确认GET /api/entryExit/page分页条件查询登记记录GET /api/entryExit/{id}查看单条登记详情GET /api/ship/page船舶档案分页查询POST /api/ship新增船舶档案GET /api/statistics/daily按日统计进出港量统一返回结构要提前定义好我常用Result对象包含code、message和data三个字段。代码结构上的清晰在答辩演x的时候非常有说服力。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }4.2 申报接口的实现状态前置校验是关键申报接口是整个系统的核心入口。这段代码里最值得琢磨的不是怎么把数据插入数据库而是插入之前要怎么判定这条申报是否合法。我的实现逻辑分三步。第一步从token或session中获取当前用户绑定的船舶id。第二步查询ship_info表判断船舶当前是否处于“在港”状态并且状态为正常。第三步才真正执行insert。出港申报里还有一道约束这条船当前不存在“未完成”的出港记录。如果系统里已经有一条audit_status0或audit_status1且未签证的出港记录就不能再申请出港。这个逻辑用一条带条件的查询就能完成// 查询是否存在未完成的出港记录 LambdaQueryWrapperEntryExitRecord wrapper new LambdaQueryWrapper(); wrapper.eq(EntryExitRecord::getShipId, shipId) .eq(EntryExitRecord::getRecordType, 1) .in(EntryExitRecord::getAuditStatus, 0, 1); Long unfinishedCount entryExitRecordMapper.selectCount(wrapper); if (unfinishedCount 0) { return Result.error(该船存在未完成的出港申请请等待审核完成); }这个校验虽然用的是普通查询但在单机低并发场景下够用了。想让逻辑更硬核一点可以在表结构里对ship_id和状态字段做一些约束配合。后面会谈到并发问题。4.3 审核与签证事务边界与条件更新审核接口要注意两个关键点一是记录审核日志二是防止重复审核。审核的状态流转逻辑代码可以在一个事务里完成。先把记录查出来判断当前状态确为“待审核”然后更新状态。更新时用条件更新来兜底Transactional(rollbackFor Exception.class) public ResultVoid audit(Long recordId, Integer auditResult, String remark) { EntryExitRecord record entryExitRecordMapper.selectById(recordId); if (record null) { return Result.error(登记记录不存在); } if (record.getAuditStatus() ! 0) { return Result.error(该记录已审核请勿重复操作); } LambdaUpdateWrapperEntryExitRecord updateWrapper new LambdaUpdateWrapper(); updateWrapper.eq(EntryExitRecord::getId, recordId) .eq(EntryExitRecord::getAuditStatus, 0) .set(EntryExitRecord::getAuditStatus, auditResult 1 ? 1 : 2) .set(EntryExitRecord::getAuditRemark, remark); int rows entryExitRecordMapper.update(null, updateWrapper); if (rows 0) { return Result.error(审核失败该记录已被其他操作员处理); } // 如果是通过审核且是出港记录将船舶状态置为“在航” if (auditResult 1 record.getRecordType() 1) { LambdaUpdateWrapperShipInfo shipWrapper new LambdaUpdateWrapper(); shipWrapper.eq(ShipInfo::getId, record.getShipId()) .set(ShipInfo::getCurrentBerthStatus, 1); shipInfoMapper.update(null, shipWrapper); } // 写入操作日志 auditLogMapper.insert(new AuditLog(recordId, getLoginUserId(), audit, remark)); return Result.ok(null); }注意这里的事务只包住了审核记录更新和船舶状态更新因为这两步是必须同时成功或同时失败的。写操作日志这一步如果也想纳入事务也不是不行但即便日志写失败也不该影响主业务我更倾向于用独立方法处理。签证接口和审核接口逻辑类似区别在于签证后船舶状态要回到“在港”因为一次出港流程走完船已经离开了后续进港时又能登记。这样一来进出港的闭环就通过船舶状态字段串起来了。5. 开发期最容易翻车的几个坑时区、事务失效、状态并发5.1 日期时间处理的时区与序列化问题进出港系统里时间字段特别多申报时间、审核时间、签证时间、实际到港时间。如果处理不好前端显示的时间和数据库对不上答辩现场演示时很容易翻车。第一个坑是MySQL连接串没配时区。JDBC连接MySQL 8时必须显式指定serverTimezone我建议直接写Asia/Shanghaispring: datasource: url: jdbc:mysql://localhost:3306/port_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai第二个坑是前后端日期格式。SpringBoot默认对LocalDateTime的序列化格式是带T的ISO格式比如2024-06-01T10:30:00前端拿到这种字符串展示很难看。统一在配置里设置格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai如果你用了MyBatis Plus的字段自动填充别忘了在实体类时间字段上配置TableField(fill FieldFill.INSERT)和对应的MetaObjectHandler否则create_time字段永远是null。5.2 Transactional失效的几种情况说到事务我见过最多的错误是事务没生效但代码看起来完全正常。常见的失效原因有三个。第一个是把Transactional写在同一个类的内部调用里。比如Controller调用本类的apply方法apply里去调本类的另一个事务方法这时事务是不生效的因为Spring的声明式事务基于代理内部调用绕过了代理对象。第二个是方法不是publicSpring事务默认不支持非public方法。第三个是自己在方法里把异常catch了事务拦截器看不到异常自然不回滚。比如你的审核方法里如果手动try-catch包住了更新操作又没throw出去那状态更新失败时前面的船舶状态更新也已经提交了脏数据就产生了。排查这类问题最直接的办法是看日志里有没有Creating new transaction字样没有就说明事务压根没开启。这个排查思路在答辩时讲出来老师会觉得你是真写过代码的。5.3 一船重复出港的并发状态变更兜底进出港登记这个场景按真实业务看并发量不高但代码上还是要把并发问题兜住。经典的错误是“先查询再更新”没有并发保护两个操作员同时审核同一条记录都先查到了“待审核”状态然后各自执行更新后执行的把先执行的覆盖掉。这种情况用我在4.3里写的条件更新可以直接规避UPDATE语句的WHERE条件里带着“当前状态必须是待审核”一旦有一个更新成功另一个更新影响行数为0就知道记录已经被处理过了。同样的逻辑也适用于船舶档案上的在港状态修改。给船舶状态字段做条件更新比在应用层用一个重量级分布式锁要简单得多。毕竟这是单体模块你的并发来源基本就是两个操作员同时点了同一个按钮条件更新完全够用。5.4 前后端联调中的字段与返回体问题联调阶段的坑也值得一提。很多同学在后端直接用实体类返回给前端于是出现这些状况前端拿不到数据因为实体里是驼峰字段而Vue里用的是下划线字段或者密码字段、deleted字段直接暴露在响应里。前者会显得很业余后者在答辩时被老师发现就很尴尬。我的做法是接口返回一律用VO对象或者至少在实体类的敏感字段上加JsonProperty(access Access.WRITE_ONLY)和JsonIgnore。密码、deleted这类字段不该出现在返回体里。VO的转换虽然多写几个类但对系统整洁度和答辩观感都有明显帮助。还有一个容易忽视的点MySQL表名和字段名尽量用下划线风格实体字段用驼峰MyBatis Plus默认开启下划线转驼峰会自动映射。如果你建的字段名用了数据库保留字比如rank、order、status的表名或列名查询时会报错。我在做这张表时特意避开了这些保留字一开始规划时就要把这些问题想清楚。6. 从及格到高分进阶模块与答辩展示的实用建议6.1 用统计报表把系统做成“数据驱动”的完整闭环基础版本做完了系统只是“能用”但距离“值得展示”还差一个统计模块。进出港登记系统的数据非常适合做可视化因为它的核心数据是时间和船舶天然可以按天、按周、按月聚合。我建议后端提供一个统计接口返回近30天的进出港数量和船舶类型分布。前端用ECharts画两个图一个折线图展示每日进出港趋势一个饼图展示船型占比。这个模块工作量不大代码也简单但效果非常直观。// 近7天出港数量统计 GetMapping(/statistics/daily) public ResultListDailyStatVO dailyStat() { LocalDate endDate LocalDate.now(); LocalDate startDate endDate.minusDays(6); ListDailyStatVO list entryExitRecordMapper.selectDailyStat(startDate, endDate); return Result.ok(list); }对应Mapper里的SQL按日期分组即可核心就是GROUP BY DATE(actual_time)。这一块在演示时很吸睛也是论文里可以大书特书的一个点。6.2 操作日志与合规追溯把“海事管理”落到系统的细节里进出港登记这个场景比普通管理系统多了一层合规属性。所以操作日志不能只做一个空壳每个审核动作、驳回动作、修改动作都应该记录操作人、操作时间、操作内容和变更前后的状态。我在系统里增加了audit_log表并在每次关键操作时手动写入一条日志。你也可以用AOP注解的方式简化代码但要确保里面写入的信息足够完备。答辩时如果老师问“系统如何处理业务纠纷和追溯问题”操作日志模块就是最好的回答材料。日志模块还能自然引出一个加分项按操作时间、操作人、操作类型的多维筛选查询。这让系统从单纯的登记工具变成一套可审计的数字化登记台账和真实港务管理需求对上了。6.3 答辩展示与演示准备关键点复盘系统做完了答辩演示也不能忽视。很多人的代码写得不错但演示时一顿乱点老师看不到重点。我建议把演示流程固定为一条业务主线注册船舶档案、提交出港申报、审核员审核、签证确认、船舶状态变为在航、提交进港申报、审核、回到在港、查看统计报表。这条线走完整个系统的业务闭环就完整了。答辩时被问最多的通常是这几个问题一条船为什么不能连续出港、审核时并发点两次会怎样、操作记录存在哪里、统计报表的数据从哪里来。每个问题对应的答案我都已经在前面章节埋好了状态机、条件更新、日志表、聚合查询都能接得上话。还有个小建议是演示之前先准备好一套完整的测试数据至少要有8-10条不同状态的登记记录和4-5条船舶档案。现场临时造数据会发生什么做过演示的人都知道。6.4 还可以继续扩展的方向如果学有余力这个系统还能继续往外延伸。比如对接消息推送让审核结果生成后以站内消息或短信方式通知船方再比如引入简单的分词搜索在船名和货物信息上实现更聪明的检索。这些都是真实系统里存在的需求也都适合作为论文里的“未来展望”章节去写。不过提醒一句不要在扩展功能上花太多时间。先确保核心的登记、审核、查询、统计四件事稳定可靠再考虑加分项。对于毕业设计而言一个业务闭环完整、代码结构清晰、遇到问题能讲清楚原理的系统已经足够拿一个好的成绩。我做完这套系统最大的感受是选题看起来冷门但越是这种带着真实业务背景的题目越能在答辩时展现出差异化的竞争力。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进