JSP+Servlet+JavaBean实现车险模拟系统:保费计算与理赔状态机
2026/9/12 6:46:43 网站建设 项目流程

简介:这份资源面向计算机及相关专业毕业生,是基于 JSP 的车险模拟系统毕业设计完整源码,覆盖保险报价、保费计算、理赔处理等核心业务,项目采用 MVC 架构,结合 Servlet、JavaBean 和数据库设计,包含用户管理、表单校验、权限控制与安全防护等模块,适合课程设计或毕业设计参考及二次开发。 压缩包共 950 个文件,约 22.48MB,主要包含 87 个 JSP 页面、21 个 Java 源文件、21 个 class 文件、70 个 jar 依赖库、20 个 XML 配置,以及 HTML、CSS、JavaScript 等前端静态资源,附带 SQL 数据库脚本和 MyHibernate/Spring 配置文件,目录结构层次清楚,便于按模块查阅和学习。目前已有 102 人参与浏览学习。 借助这套源码,读者可以快速搭建可运行环境,理解车险业务系统中报价、投保、理赔等流程的设计与实现思路,掌握 JSP 动态页面开发、MVC 分层架构、数据库建模、事务处理和安全防护等关键技能。同时,资源还覆盖了测试、部署与性能优化等实践要点,是一份兼具教学参考与工程实战价值的毕业设计参考资料。

1. 从毕业设计标题出发的系统定位

“基于JSP的车险模拟系统”这类题目在毕业设计里出镜率不低,但多数实现只是把增删改查搬进网页,答辩时老师一句“商业险保费里的费率浮动表放在哪里算的”就答不上来。真正的车险模拟系统,核心不是“录数据”,而是要用代码模拟出保险业务里的两套关键规则:保费计算规则和理赔状态推进规则。前者是精算模型的简化版,后者是业务流程的状态机。本文按 JSP + Servlet + JavaBean 的经典三层结构,把一张保单从创建、报价到出险理赔的完整链路讲清楚,附带可复用的建表语句、JDBC接入代码和页面实现思路,给做毕设的同学一条能跑通、能解释、能抗提问的实现路径,也给想快速上手 JSP 全栈开发的读者一份可参考的落地样板。

2. 技术选型与MVC分层:JSP + Servlet + JavaBean的职责边界

2.1 为什么这个题目不必硬上 Spring 框架

毕设选型的原则是“够用且能自证”。车险模拟系统的业务量不大,用户并发极低,核心是业务逻辑的表达,上 Spring Boot 反而会让老师把提问重点移到“自动配置、IOC 原理”等框架机制上,偏离车险业务本身。JSP + Servlet + JavaBean 是 Java Web 课程里的标准答案,三个部件刚好对应 MVC 三层。

层次组件职责对应包名/目录
展示层JSP表单收集、数据回显、页面跳转WebContent/jsp/
控制层Servlet参数接收、调用业务层、转发重定向src/controller/
业务层JavaBean保费计算、理赔状态机、数据校验src/service/
数据层JavaBean + JDBC数据库访问、结果集封装src/dao/

这里没有给数据层单独造术语,JavaBean 既可以做业务对象,也可以承载 DAO 职责,毕设里不引入 MyBatis 也完全说得通。关键是让每个类的名字能直接看出它属于哪一层,包名清晰,web.xml 里配好 Servlet 路径,代码质量这一项的答辩分就能拿住。

2.2 项目目录结构与关键配置

一个能直接导入 Eclipse 或 IDEA(配置 Tomcat 运行)的项目结构长这样:

CarInsuranceSimulator/ ├── src/ │ ├── model/ // 实体Bean: Vehicle, Policy, ClaimRecord │ ├── dao/ // 数据访问: VehicleDao, PolicyDao, ClaimDao │ ├── service/ // 业务逻辑: PremiumCalculator, ClaimService │ ├── controller/ // Servlet: LoginServlet, QuoteServlet, ClaimServlet │ └── util/ // 工具类: DBUtil, DateUtil ├── WebContent/ │ ├── jsp/ // 页面: login.jsp, quote.jsp, policyList.jsp │ ├── css/ // 样式文件 │ ├── static/ │ └── WEB-INF/ │ └── web.xml // Servlet 3.0 配置

WebContent 目录下放的是 Tomcat 直接对外暴露的静态资源和 JSP 页面,WEB-INF 下的 web.xml 是容器启动时加载的主配置。页面文件通常不直接放进 WEB-INF 里,因为放在 WEB-INF 下只能通过 forward 访问,虽能防止直接 URL 访问页面,但开发调试时反而麻烦。毕设项目更建议 JSP 放 jsp 目录下,用 Filter 做访问控制,这样页面改完刷新就能看到效果。

2.3 web.xml 中 Servlet 映射与初始化参数的写法

web.xml 是 JSP 项目的脊柱,Servlet 3.0 支持注解方式,但毕业设计里把映射写进 web.xml 更利于答辩展示配置逻辑。以报价请求为例:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>CarInsuranceSimulator</display-name> <!-- 编码过滤器:统一请求与响应编码 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>util.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 登录拦截过滤器 --> <filter> <filter-name>LoginFilter</filter-name> <filter-class>util.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/jsp/*</url-pattern> </filter-mapping> <!-- 报价Servlet --> <servlet> <servlet-name>QuoteServlet</servlet-name> <servlet-class>controller.QuoteServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>QuoteServlet</servlet-name> <url-pattern>/quote</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> </web-app>

这段配置里有三个细节值得答辩时展开。第一是 filter-mapping 的 url-pattern 用了/jsp/*,表示只拦截页面请求,不做 API 级权限控制,简单项目这样写足够。第二是 load-on-startup 为 1 时,Servlet 在 Tomcat 启动时就完成初始化,避免首次访问时编译 Servlet 带来的延迟。第三是编码过滤器必须放在所有过滤器最前面,否则后面读到的请求参数还是 ISO-8859-1 编码,中文直接乱码。乱码问题在 JSP 项目中占排障时间的三成以上,把编码过滤器写在最上面是第一个经验点。

请求流转到这里已经清晰:浏览器发起/quote请求,web.xml 将 URL 交给 QuoteServlet,Servlet 调用 service 层计算保费,结果放到 request 域并 forward 到 quoteResult.jsp,JSP 通过 EL 表达式取出数据渲染页面。三层之间的调用边界明确,答“一次请求经历了什么”时,按这个链路说就是满分回答。

3. 数据库设计与JDBC封装:车险核心数据的表结构与连接管理

3.1 保单、车辆、理赔三张核心表的字段设计

车险系统绕不开一张保单的完整生命周期:先有车辆信息,然后投保生成保单,出险后创建理赔单。数据库设计围绕这三个对象展开,字段要为保费计算和理赔金额计算服务,而不是简单堆字段。

CREATE DATABASE IF NOT EXISTS car_insurance DEFAULT CHARACTER SET utf8mb4; USE car_insurance; -- 车辆信息表 CREATE TABLE vehicle ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL COMMENT '车牌号', owner_name VARCHAR(50) NOT NULL COMMENT '车主姓名', owner_phone VARCHAR(20) COMMENT '联系电话', vehicle_type TINYINT NOT NULL COMMENT '1-家庭自用 2-营运货车 3-营运客车', purchase_price DECIMAL(10,2) NOT NULL COMMENT '新车购置价(元)', reg_date DATE NOT NULL COMMENT '首次登记日期', seats INT DEFAULT 5 COMMENT '核定载客数', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='车辆档案表'; -- 保单表 CREATE TABLE insurance_policy ( id INT PRIMARY KEY AUTO_INCREMENT, policy_no VARCHAR(32) NOT NULL UNIQUE COMMENT '保单号', vehicle_id INT NOT NULL COMMENT '关联车辆ID', compulsory_premium DECIMAL(10,2) NOT NULL COMMENT '交强险保费', commercial_premium DECIMAL(10,2) NOT NULL COMMENT '商业险保费', total_premium DECIMAL(10,2) NOT NULL COMMENT '总保费', start_date DATE NOT NULL COMMENT '起保日期', end_date DATE NOT NULL COMMENT '终保日期', status TINYINT DEFAULT 1 COMMENT '1-有效 2-已过期 3-已退保', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_policy_vehicle FOREIGN KEY (vehicle_id) REFERENCES vehicle(id) ) ENGINE=InnoDB COMMENT='保单表'; -- 理赔记录表 CREATE TABLE claim_record ( id INT PRIMARY KEY AUTO_INCREMENT, claim_no VARCHAR(32) NOT NULL UNIQUE COMMENT '报案号', policy_id INT NOT NULL COMMENT '关联保单ID', accident_date DATE NOT NULL COMMENT '出险日期', accident_desc VARCHAR(255) COMMENT '事故描述', responsibility_ratio DECIMAL(4,2) DEFAULT 1.00 COMMENT '责任比例(0~1)', loss_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '定损金额', deduct_ratio DECIMAL(4,2) DEFAULT 0.20 COMMENT '免赔率(0~1)', payable_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '最终赔付金额', status TINYINT DEFAULT 1 COMMENT '1-报案 2-定损 3-待核赔 4-已赔付', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_claim_policy FOREIGN KEY (policy_id) REFERENCES insurance_policy(id) ) ENGINE=InnoDB COMMENT='理赔记录表';

三个表之间形成“车辆 1 对多 保单,保单 1 对多 理赔单”的级联关系。责任比例 responsibility_ratio 和免赔率 deduct_ratio 分开存放,是业务上的关键区分:责任比例决定保险公司赔不赔,免赔率决定赔多少打折扣。两列都保留,后期统计赔付率时才能分别按责任和免赔维度过滤。varchar 类型统一用 utf8mb4 字符集,避免生僻汉字或特殊符号写入时报 Incorrect string value 错误。

3.2 JDBC 连接管理:从 DriverManager 到连接池的演进

JSP 毕设直接用 DriverManager 写死连接是扣分项,每次请求新建连接的开销虽然小但体现不出工程素养。用commons-dbcp2连接池把数据库连接复用起来,代码量多一二十行,但能写出“数据库资源为什么不能反复创建”的答案。

package util; import org.apache.commons.dbcp2.BasicDataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { private static final BasicDataSource dataSource = new BasicDataSource(); static { dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/car_insurance" + "?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMaxTotal(20); dataSource.setMaxIdle(10); dataSource.setMinIdle(2); dataSource.setMaxWaitMillis(3000); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps != null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

静态代码块在类加载时执行一次,数据源全局共享。setMaxWaitMillis 设为 3000 毫秒,连接池耗尽时请求最多等 3 秒就抛异常,避免线程无限阻塞。关闭资源时按 ResultSet → PreparedStatement → Connection 的倒序释放,连接池的 close 方法归还连接而不是真正关闭,这是和 DriverManager 模型最大的区别。

3.3 PreparedStatement 预编译:防 SQL 注入的基本姿势

JSP 项目最容易暴露的安全问题就是字符串拼接 SQL。车险系统的保单查询条件多,报价输入项杂,Statement 拼接的查询条件很容易被构造出恒真条件。PreparedStatement 用占位符?绑定参数,数据库层面先预编译 SQL 骨架,再传入参数,用户输入中的单引号会按普通字符处理,没有机会改变 SQL 语义。

package dao; import util.DBUtil; import model.Vehicle; import java.sql.*; import java.util.ArrayList; import java.util.List; public class VehicleDao { public List<Vehicle> queryByOwnerName(String keyword) { // 注意?号位置,参数可以通过setString绑定,不允许改变SQL骨架 String sql = "SELECT id, plate_no, owner_name, vehicle_type, " + "purchase_price, reg_date FROM vehicle " + "WHERE owner_name LIKE ?"; List<Vehicle> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Vehicle v = new Vehicle(); v.setId(rs.getInt("id")); v.setPlateNo(rs.getString("plate_no")); v.setOwnerName(rs.getString("owner_name")); v.setVehicleType(rs.getInt("vehicle_type")); v.setPurchasePrice(rs.getBigDecimal("purchase_price")); v.setRegDate(rs.getDate("reg_date").toLocalDate()); list.add(v); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public int insertVehicle(Vehicle v) { String sql = "INSERT INTO vehicle (plate_no, owner_name, owner_phone, " + "vehicle_type, purchase_price, reg_date, seats) " + "VALUES (?,?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, v.getPlateNo()); ps.setString(2, v.getOwnerName()); ps.setString(3, v.getOwnerPhone()); ps.setInt(4, v.getVehicleType()); ps.setBigDecimal(5, v.getPurchasePrice()); ps.setDate(6, Date.valueOf(v.getRegDate())); ps.setInt(7, v.getSeats()); int rows = ps.executeUpdate(); if (rows > 0) { try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { v.setId(keys.getInt(1)); } } } return rows; } catch (SQLException e) { e.printStackTrace(); return 0; } } }

try-with-resources 写法让连接只用声明一次,省去 finally 里逐个判断非空再关闭的模板代码。通配符查询中 LIKE 的%也绑定在参数里拼接,拼进参数值不影响 SQL 层级。getGeneratedKeys 拿到自增主键,解决“插入车辆后立刻用来关联保单”的场景。这种写法放在 DAO 层每个方法里重复出现,虽然啰嗦,但答辩时能明确回答“如何查出一条数据、如何取回自增 ID”。

4. 业务核心落地:保费计算、保单状态与理赔流程实现

4.1 车险保费计算规则的精简模型与 Java 实现

真实车险的费率精算涉及大量历史赔付数据和行业系数,毕设只需要保留业务表达逻辑:交强险按车辆种类和座位数取固定基准值,商业险按新车购置价 × 基础费率 × 使用年限折扣。核心代码的职责是“把规则写成可解释的方法”,而不是算出行业级精度。

package service; import model.Vehicle; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.Period; public class PremiumCalculator { // 基础商业险费率:明显低于实际精算费率,此处仅为模拟 private static final BigDecimal COMMERCIAL_BASE_RATE = new BigDecimal("0.0120"); // 交强险基准保费:1-家庭自用按950元起,营运货车按1800元起 public BigDecimal calcCompulsoryPremium(Vehicle v) { if (v.getVehicleType() == 1) { return new BigDecimal("950.00"); } else if (v.getVehicleType() == 2) { return new BigDecimal("1800.00"); } else { return new BigDecimal("2200.00"); } } public BigDecimal calcCommercialPremium(Vehicle v) { BigDecimal baseAmount = v.getPurchasePrice() .multiply(COMMERCIAL_BASE_RATE); // 使用年限折扣:每年折5%,上限折到60% int ageYears = Period.between(v.getRegDate(), LocalDate.now()).getYears(); BigDecimal discount = BigDecimal.ONE.subtract( new BigDecimal(ageYears).multiply(new BigDecimal("0.05"))); if (discount.compareTo(new BigDecimal("0.60")) < 0) { discount = new BigDecimal("0.60"); } return baseAmount.multiply(discount) .setScale(2, RoundingMode.HALF_UP); } public BigDecimal calcTotalPremium(Vehicle v) { return calcCompulsoryPremium(v).add(calcCommercialPremium(v)); } }

BigDecimal 是金额计算的强制选择,double 浮点误差在金额上会引发“保费多了几分钱”的尴尬问题。setScale(2, RoundingMode.HALF_UP) 是四舍五入到分的标准写法。使用年限 discount 的封底 0.60 防止旧车保费无限向下折扣,模拟了精算模型中的最低折扣系数。答辩时可以把这些系数改成从数据库配置表读取,这样“参数配置化”的回答就能接上了。

4.2 理赔流程的状态推进实现

理赔是车险系统里最有动态感的模块:报案 → 定损 → 核赔 → 赔付。每个状态改变都要更新赔付金额相关字段,而且系统要保证状态只能向后推进。这里的实现策略是 ClaimService 接收前端传递的“当前状态 + 操作类型”,内部校验后再执行更新。

package service; import model.ClaimRecord; import java.math.BigDecimal; import java.math.RoundingMode; public class ClaimService { // 状态推进规则:1报案 -> 2定损 -> 3核赔 -> 4已赔付 public int applyNextStatus(ClaimRecord claim, String action) { int current = claim.getStatus(); if ("report".equals(action)) { // 定损后要计算赔付金额:定损额 × 责任比例 × (1-免赔率) BigDecimal loss = claim.getLossAmount(); BigDecimal ratio = claim.getResponsibilityRatio(); BigDecimal deduct = claim.getDeductRatio(); BigDecimal payable = loss.multiply(ratio) .multiply(BigDecimal.ONE.subtract(deduct)) .setScale(2, RoundingMode.HALF_UP); claim.setPayableAmount(payable); return 2; } else if ("survey".equals(action)) { return 3; } else if ("approve".equals(action)) { return 4; } // 非预期操作则保持原状态 return current; } public boolean canTransit(int fromStatus, String action) { if (fromStatus == 1 && "report".equals(action)) return true; if (fromStatus == 2 && "survey".equals(action)) return true; if (fromStatus == 3 && "approve".equals(action)) return true; return false; } }

定损节点的计算是重点:上一步定损操作录入 loss_amount、responsibility_ratio、deduct_ratio,审核时用公式计算出 payable_amount。公式体现了保险公司理赔的核心逻辑——赔付金额并不等于定损金额,要按责任比例打折再扣掉免赔部分。canTransit 方法把状态机校验和业务逻辑分开,防止前端越过中间状态直接提交到已赔付。

4.3 Servlet 接收报价参数与 JSP 回显的完整链路

上面两层业务逻辑最终要通过 Servlet 暴露给页面。报价表单提交时,Servlet 负责收集车辆参数、调用计算器、返回结果页面。

package controller; import model.Vehicle; import service.PremiumCalculator; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigDecimal; import java.time.LocalDate; public class QuoteServlet extends HttpServlet { private PremiumCalculator calculator = new PremiumCalculator(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 收取前端传的车辆参数并封装成Vehicle对象 Vehicle v = new Vehicle(); v.setPlateNo(req.getParameter("plateNo")); v.setOwnerName(req.getParameter("ownerName")); v.setVehicleType(Integer.parseInt(req.getParameter("vehicleType"))); v.setPurchasePrice(new BigDecimal(req.getParameter("purchasePrice"))); v.setRegDate(LocalDate.parse(req.getParameter("regDate"))); v.setSeats(Integer.parseInt(req.getParameter("seats"))); // 调用业务层计算结果 BigDecimal compulsory = calculator.calcCompulsoryPremium(v); BigDecimal commercial = calculator.calcCommercialPremium(v); BigDecimal total = compulsory.add(commercial); // 结果放入request作用域,forward到结果页回显 req.setAttribute("vehicle", v); req.setAttribute("compulsory", compulsory); req.setAttribute("commercial", commercial); req.setAttribute("total", total); req.getRequestDispatcher("/jsp/quoteResult.jsp").forward(req, resp); } }

Servlet 里只做参数接收和页面跳转,不放置任何计算逻辑。req.getAttribute 之后用 forward 跳转,地址栏不变,刷新页面会导致重复提交,这是毕设里可以主动提的一个已知缺点,然后引出用 PRG 模式解决的方案。对应 JSP 页面的结果展示区用 EL 表达式取值:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <body> <h3>报价结果</h3> <p>车牌号:${vehicle.plateNo}</p> <p>车辆类型: ${vehicle.vehicleType == 1 ? '家庭自用' : '营运车辆'} </p> <p>交强险保费:${compulsory} 元</p> <p>商业险保费:${commercial} 元</p> <p><strong>总保费:${total} 元</strong></p> <a href="jsp/quote.jsp">返回重新报价</a> </body> </html>

JSP 中尽量不写 Java 脚本片段,只用 EL 表达式 ${} 取值,三元运算符做显示层的简单分支。这样页面看起来干净,答“表现层和数据层如何解耦”时可以直接拿这页举例。

5. 权限控制与页面细节:登录拦截、防重复提交与数据校验

5.1 用 Filter 实现统一登录拦截

车险系统里保单和理赔数据属于业务内部数据,不做登录限制说不过去。Filter 拦截所有 jsp 请求,Session 里没有登录标记就重定向到登录页。

package util; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 登录页面本身不用拦截,否则会死循环跳转 if (uri.endsWith("/login.jsp") || uri.contains("/login")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); Object loginUser = (session != null) ? session.getAttribute("loginUser") : null; if (loginUser == null) { // 未登录则重定向到登录页,附带来源URL便于登录后回跳 response.sendRedirect(request.getContextPath() + "/login.jsp" + "?redirect=" + request.getRequestURI()); return; } chain.doFilter(req, resp); } }

getSession(false) 是这里的关键细节:false 表示如果没有会话则返回 null,避免为每个未登录请求都创建一个无意义的 HttpSession 对象。放行登录页的逻辑不能漏,否则过滤器把登录页自己也拦截了,形成重定向死循环。sendRedirect 之后要加 return,防止 filter 链继续往下走导致页面输出异常。

5.2 表单重复提交的两种拦截方案

报价和理赔提交页面如果直接 forward,用户按 F5 就会重新走一遍 doPost,产生两条保单记录。常见的处理是 PRG 模式,即 Post-Redirect-Get。也就是 Servlet 写库成功之后,用 sendRedirect 跳转到结果展示 servlet,而不是 forward。另一种做法是在表单里放一个隐藏 token:

<% String token = java.util.UUID.randomUUID().toString(); session.setAttribute("formToken", token); %> <form action="/quote" method="post"> <input type="hidden" name="formToken" value="<%= token %>"/> <!-- 车辆信息字段 --> </form>
// 在QuoteServlet的doPost开头校验token HttpSession session = req.getSession(); String sessionToken = (String) session.getAttribute("formToken"); String requestToken = req.getParameter("formToken"); if (sessionToken == null || !sessionToken.equals(requestToken)) { resp.sendError(400, "重复提交"); return; } session.removeAttribute("formToken"); // 一次有效,防止绕过

该方案的核心是一次性 token 校验:提交成功后立即删除 session 中的 formToken,刷新页面时参数里的 token 已经与 session 中的不匹配,请求被拒绝。这种实现放在 JSP 里用脚本片段取 UUID,虽然略有些违背“页面不写 Java”的原则,但作为防重提交手段在毕设中可接受,答辩时主动提出该设计即可。

5.3 JSP 层面四类参数校验的取舍

服务端校验才是安全防线,前端 JS 校验只是用户体验优化。对车险系统而言,重点检查四类数据。

校验对象验证规则服务端实现位置
车牌号正则匹配 省份简称 + 字母数字组合VehicleService.validatePlate
购置价> 0 且不超过 500 万元PremiumCalculator 前置检查
首次登记日期不晚于当前日期日期解析时比较 LocalDate.now()
出险日期不得早于保单起保日ClaimService.checkAccidentDate
// 参考实现片段:金额和日期的组合校验 public boolean validateQuoteParams(Vehicle v) { if (v.getPurchasePrice().compareTo(BigDecimal.ZERO) <= 0) { return false; // 金额不能为0或负数 } if (v.getRegDate().isAfter(LocalDate.now())) { return false; // 首次登记日期不能在未来 } if (v.getSeats() < 1 || v.getSeats() > 20) { return false; // 座位数合理范围 } return true; }

金额比较必须用 compareTo 而不是 equals,因为 BigDecimal 的 equals 会连小数点后的精度一起比较,0.00 和 0 在 equals 下不相等,而 compareTo 只比数值大小。JSP 页面端可以再加一层 JS 拦截,但服务端校验才是真墙,这个顺序反了就是安全漏洞。

6. 答辩演示准备与代码提交前的检查清单

6.1 给演示数据库注入一组有业务含义的测试数据

空库演示是最扣分的场景。演示前在 MySQL 中手工执行几条 INSERT,数据要覆盖不同状态:一辆家庭自用车、一辆营运货车,各对应一份保单,其中一辆车有一笔理赔流程停在定损状态,另一笔走完赔付闭环。运行时的保费计算结果与手工 Excel 预演结果提前对过即可现场回答问题。

-- 家庭自用车示例,购置价150000元,2020年登记(约5年车龄) INSERT INTO vehicle (plate_no, owner_name, owner_phone, vehicle_type, purchase_price, reg_date, seats) VALUES ('京A12345', '张三', '13800138000', 1, 150000.00, '2020-03-15', 5); -- 为该车生成一张有效保单,保单号预生成以展示唯一约束 INSERT INTO insurance_policy (policy_no, vehicle_id, compulsory_premium, commercial_premium, total_premium, start_date, end_date, status) SELECT 'P20240010001', id, 950.00, 1260.00, 2210.00, '2024-01-01', '2024-12-31', 1 FROM vehicle WHERE plate_no = '京A12345';

注意保单号使用业务编码规则“P + 年份 + 6位序号”,这个格式在真实保险系统里具有实际意义,提前在数据库层面加 UNIQUE 约束后就具备了防重能力,比程序里手动判断再插入更可靠。

6.2 源码检查:答辩前需要过一遍的七个点

代码提交时不要只贴整个 src 目录。检查这几个具体位置,每一项都可能成为答辩提问切入点:

  • web.xml 中是否统一配置了 UTF-8 编码过滤器,JSP 页面头部的 pageEncoding 是否一致。
  • DAO 中是否还有 Statement 拼接字符串的残留写法,全部替换为 PreparedStatement。
  • 连接是否在 finally 或 try-with-resources 中正确关闭,无法关闭的连接会造成连接池耗尽。
  • Session 中存储的对象是否实现 Serializable,未实现的 session 持久化时会报 NotSerializableException。
  • JSP 页面是否还在用 <%= %> 脚本片段输出大量数据,尽量用 EL 替换。
  • 理赔状态推进时是否有字段级校验,比如定损金额未填时不能进入核赔。
  • 数据库配置文件中的账号密码是否硬编码在 DBUtil 中,可改为外部 properties 文件读取。

6.3 命令行快速验证项目可运行状态

如果在 Linux 服务器上演示或做环境迁移,不用启动 Eclipse 也能快速验证项目是否可跑。提前将项目打成 war 包部署到 Tomcat,然后依次检查三个健康信号:

# 1. 项目部署后确认目录解压成功 ls -la /usr/local/tomcat/webapps/car_insurance/ # 2. 确认数据库连接能建立(MySQL Container场景下) mysql -h127.0.0.1 -uroot -p123456 -e "use car_insurance; show tables;" # 3. 用curl模拟一次登录请求,观察Cookie与响应头 curl -i -X POST http://localhost:8080/car_insurance/login \ -d "username=admin&password=123456" -c cookies.txt

curl -c 参数将服务器下发的 JSESSIONID 写入本地文件,后续访问受保护页面时带上-b cookies.txt,就能验证 Filter 拦截逻辑是否正确放行已登录用户。这三个命令跑通之后,项目对于你而言才真正可控,不再依赖 IDE 内置浏览器确认效果。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询