简介:这是一套基于JavaWeb技术栈构建的金融借贷系统毕业设计资源,定位于P2P金融管理、小额贷款场景,适合计算机相关专业学生用于毕业设计或项目实训。系统后台使用Servlet、JDBC与FileUpload,界面采用BootStrap、jQuery与Ajax,数据存储基于MySQL,整体分为前台与后台:前台支持融资产品查询、产品详情、贷款申请、每日新闻浏览,后台提供贷款申请审批、融资产品管理、产品类型与周期管理、新闻管理、企业管理、密码修改等功能,业务环节完整且界面简洁易用。资源包共15个文件,包含项目源码(zip)、数据库脚本(sql)、项目文档(pdf/md)、运行截图(png)和软件工具下载说明(txt),压缩包大小14.67MB,目录结构清晰,运行截图可直观预览最终效果,软件工具说明便于环境配置与部署。目前已有2536人学习下载,通过完整源码、数据库脚本和说明文档,可快速搭建一套可演示的金融借贷系统,经严格调试可直接运行,适合作为毕设成果或项目实战练手。
1. 金融借贷系统:JavaWeb 毕设里最能打的题目之一
金融借贷系统是 JavaWeb 毕设里少见的能同时覆盖用户、交易、审核、统计四个层面的题目,而且它天然带着“资金安全”这个硬约束,比普通增删改查系统更容易在答辩时讲出深度。很多同学选这个题,看中的就是它“附源码”三个字——拿到手能跑、能改、能讲清楚。但它恰恰也是踩坑大户:金额精度、并发放款、状态流转,任何一个环节想糊弄过去,导师一眼就能看出来。适合谁做?适合已经学过 Servlet、JSP、MySQL,想用一个完整案例把 JavaWeb 全链路串起来的人。这篇笔记会按我从零搭这类项目的习惯,把数据模型、核心代码、本地跑通和翻车点一次讲透。
2. 从建表开始:先把借贷系统的数据模型和状态机定死
2.1 技术选型:Servlet + JSP + MyBatis 为什么是稳妥组合
毕设评分最看重的是“你能把每一层讲明白”。Spring Boot 虽然写起来爽,但很多同学答辩时连自动装配都说不清,反而减分。我一般建议金融借贷系统这类题目用 Servlet + JSP + MyBatis 的组合:Controller 层用 Servlet,自己控制请求分发;Service 层写业务逻辑,事务边界放在这里;DAO 层用 MyBatis,SQL 自己写在 mapper 里。
这个组合的好处第一是分层干净,第二是面试官或导师问“一个请求从 URL 到数据库经历了什么”,你能一条线讲到底。另一个现实原因是这类毕设源码绝大多数都是这个结构,你拿到手之后对照着看,半小时就能把目录结构摸清,后面改代码不会迷路。
如果你手上的项目是 Spring MVC 版本,也完全没问题,核心业务代码其实只差一层注解。下面所有关于业务实现的逻辑都是通用的,只是我按最经典的 Servlet 方式来写代码。
2.2 核心表设计:用户、借款标、投资、还款计划
金融借贷系统的数据库设计是整个项目的命根子,表结构不对,后面写十遍代码都救不回来。我见过很多翻车项目,问题都不是出现在代码上,而是表里没有状态字段、金额用 DOUBLE、没有索引。按我的习惯,最少要有五张表:用户表、借款标表、投资记录表、还款计划表、资金流水表。
用户表是最基础的,但要注意余额字段必须用 DECIMAL,不能用 DOUBLE。DECIMAL(12,2) 表示最大 10 位整数加 2 位小数,对个人借贷场景已经足够。
CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL, `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0-投资人 1-借款人 2-管理员', `real_name` VARCHAR(30) DEFAULT NULL, `id_card` VARCHAR(18) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;role 字段决定了整个系统的权限边界,默认 0 是投资人,注册时由页面传入。答辩时导师大概率会问“怎么防止用户注册时把自己 role 改成 2”,这是一个很好的加分点——正确做法是后端强制从 session 里取注册方式,而不是信任前端传参。
借款标表是整个借贷流程的核心,状态字段尤其重要:
CREATE TABLE `loan` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `borrower_id` INT NOT NULL, `title` VARCHAR(100) NOT NULL, `amount` DECIMAL(12,2) NOT NULL COMMENT '借款总额', `rate` DECIMAL(5,2) NOT NULL COMMENT '年化利率,8.00 表示 8%', `term_months` INT NOT NULL COMMENT '借款期限,单位月', `repay_method` TINYINT NOT NULL DEFAULT 0 COMMENT '0-等额本息 1-先息后本', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待审核 1-募集中 2-还款中 3-已完成 4-已拒绝 5-已流标', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_borrower` (`borrower_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里我故意加了 idx_status 索引,因为业务里最频繁的查询就是“列表页按状态刷借款标”,没有这个索引,数据量一旦上来,列表接口会明显变慢,答辩演示时容易卡顿。
投资记录表负责记录“谁投了哪个标多少钱”,注意这里只有金额,不存利率——利率要从借款标表带出来,避免冗余字段在两处不一致:
CREATE TABLE `invest` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `loan_id` INT NOT NULL, `user_id` INT NOT NULL, `amount` DECIMAL(12,2) NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_loan` (`loan_id`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;还款计划表则是把一笔贷款拆成每个月要还的本金和利息,这是金融系统里最能体现你理解业务深度的表:
CREATE TABLE `repayment_plan` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `loan_id` INT NOT NULL, `period` INT NOT NULL COMMENT '第几期,从1开始', `due_date` DATE NOT NULL, `principal` DECIMAL(12,2) NOT NULL COMMENT '本期应还本金', `interest` DECIMAL(12,2) NOT NULL COMMENT '本期应还利息', `total` DECIMAL(12,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待还 1-已还 2-逾期', KEY `idx_loan` (`loan_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status 在还款计划表里定义为 2 表示逾期,是因为借贷业务需要在还款日之后跑定时任务检查逾期状态,答辩时提到这里能显得你想过真实运营需求。
2.3 借款订单的状态机:从待审核到已流标
贷款状态是这套系统里最容易讲不清楚的部分。0 到 5 六个状态必须严格按顺序流转,而且状态的迁移只能由特定角色触发:用户发起借款时置为 0 待审核,管理员审核通过后置为 1 募集中,募集满额后置为 2 还款中,还款计划全部结清置为 3 已完成。另外两个分支状态是 4 已拒绝和 5 已流标——流标指的是在募集期限内没有凑满金额。
我见过很多同学在 Service 层写更新状态时,直接用 UPDATE 语句改 status,不做前置状态校验,结果就是用户能重复审核、投资人能投已经完成的标。正确写法是在 SQL 里把当前状态作为条件写进 WHERE:
UPDATE loan SET status = 2 WHERE id = #{loanId} AND status = 1这条 SQL 的影响行数只有 1 的时候才说明状态迁移成功,否则说明当前状态已经不是募集中,前端需要提示刷新页面。这是一个典型的并发安全写法,后面避坑章节会再展开。
3. 把核心链路写通:借款申请、审核、放款、还款的代码实现
3.1 项目分层与登录校验:不让未登录请求进入业务
拿到这类项目源码后,我建议第一个看的位置是目录结构——不是看代码,是看包名是否按三层结构分开。规范的项目一般有 controller、service、mapper、entity 四个包。如果源码把所有东西都堆在 Servlet 里,那这个源码就算能跑,你也很难改,我的建议是重新分层。
登录校验在金融系统里是安全底线,不可能每个 Servlet 开头都写一遍判断用户是否登录。最常见也是最稳妥的做法是写一个过滤器,把所有需要登录的请求路径都拦下来:
@WebFilter("/user/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?redirect=" + request.getRequestURI()); return; } chain.doFilter(req, resp); } }这段代码的逻辑不复杂:命中 /user/ 开头的请求先看 session 里有没有 loginUser,没有就跳回登录页,并且把原始请求路径带回去,登录成功后可以跳回来。注意这个重定向地址要拼接上项目的 contextPath,很多新手直接写 login.jsp,Tomcat 会报 404,这是配置上最常见的坑。
过滤器的注册方式有两种,一种是上面代码里的 @WebFilter 注解加 @ServletComponentScan,另一种是在 web.xml 里配 filter-mapping。如果源码是纯注解方式,启动类或 ServletContainerInitializer 里记得开启组件扫描。这个细节在 IDEA 里运行项目时最容易翻车,后面第四章会讲到。
3.2 借款审核的 Service 实现与状态流转代码
借款标审核这一节,我要完整走一遍“用户发布借款申请 → 管理员审核 → 标的上线募集”的流程。用 Servlet 收请求,Service 做业务判断,Mapper 操作数据库。这个链路写通之后,整个项目你就掌握了八成。
首先是借款人提交借款申请,Servlet 收到表单参数后组装成 Loan 对象:
@WebServlet("/borrow/apply") public class BorrowApplyServlet extends HttpServlet { private LoanService loanService = new LoanService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { User user = (User) req.getSession().getAttribute("loginUser"); Loan loan = new Loan(); loan.setBorrowerId(user.getId()); loan.setTitle(req.getParameter("title")); loan.setAmount(new BigDecimal(req.getParameter("amount"))); loan.setRate(new BigDecimal(req.getParameter("rate"))); loan.setTermMonths(Integer.parseInt(req.getParameter("term"))); loan.setRepayMethod(Integer.parseInt(req.getParameter("repayMethod"))); loan.setStatus(0); loanService.applyLoan(loan); resp.sendRedirect(req.getContextPath() + "/user/myLoans.jsp"); } }注意 setAmount 这一行,页面传来的字符串必须先转成 BigDecimal,不能传 String 到 Service 里再转。原因是后面所有金额计算都会基于这个对象,早转早安心,避免 Service 里到处都是强制转换。
LoanService 的 applyLoan 方法里,除了插入借款标,还要顺手生成还款计划。这里有个业务细节要处理好:还款计划要在“审核通过”时生成,还是“募集满额”时生成?我的做法是募集满额后生成,因为状态变成 2 还款中才有了真正需要还的钱。否则标如果流标了,数据库中会残留一份没有意义的还款计划。
审核的 Service 方法要同时更新两件事:标的状态和管理员操作记录。虽然我们没有建管理员操作日志表,但至少状态更新必须放在事务里:
public void approveLoan(Long loanId) { SqlSession session = MyBatisUtil.getSession(); try { LoanMapper mapper = session.getMapper(LoanMapper.class); // 关键:WHERE 里带 status = 0,防止重复审核 int rows = mapper.updateStatus(loanId, 2, 0); if (rows != 1) { throw new RuntimeException("当前状态下不能审核"); } session.commit(); } catch (Exception e) { session.rollback(); throw e; } finally { session.close(); } }这里updateStatus(loanId, 2, 0)的三个参数分别是主键、新状态、期望的旧状态。把旧状态放进 WHERE 条件是一个非常有用的习惯,它把并发问题从代码层面挡掉了大半,而且是所有 DAO 层框架都支持的写法。如果你用的是 Spring 管理事务,把 @Transactional 加在方法上就好,但底层逻辑不变。
3.3 等额本息计算:BigDecimal 版本的还款计划生成
还款方式里最常出现的就是等额本息和先息后本。先息后本实现简单,每月只还利息,最后一期还本金加利息。等额本息的难点在公式推导和浮点精度上,这部分代码写对了,答辩时可以直接押题。
等额本息月供公式是:月供 = 本金 × 月利率 × (1+月利率)^期数 ÷ ((1+月利率)^期数 - 1)。我见过很多同学用 double 直接套公式,结果金额出现 0.001 的偏差,原因是浮点数二进制存储导致精度丢失。正确做法是全程用 BigDecimal:
public BigDecimal calcMonthlyPayment(BigDecimal principal, BigDecimal annualRate, int months) { // 年化利率转成月利率,保留8位小数避免后续乘除误差放大 BigDecimal monthlyRate = annualRate .divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP) .divide(new BigDecimal("12"), 8, RoundingMode.HALF_UP); BigDecimal onePlus = BigDecimal.ONE.add(monthlyRate); // BigDecimal.pow 只接受 int 指数 BigDecimal pow = onePlus.pow(months); BigDecimal numerator = principal.multiply(monthlyRate).multiply(pow); BigDecimal denominator = pow.subtract(BigDecimal.ONE); // 月供保留2位小数,四舍五入 return numerator.divide(denominator, 2, RoundingMode.HALF_UP); }注意年利率到月利率的转换必须分两步除,先除 100 转换百分数,再除 12 转月利率,每一次都指定保留位数和舍入模式。在 BigDecimal 运算里所有除不尽的场景都必须显式指定精度,否则直接抛 ArithmeticException。这个代码跑出来自动驱逐查找bug效应——所以每月还款明细都要用这份月供循环生成。
有了月供之后,生成整个还款计划的循环逻辑如下:
public List<RepaymentPlan> generatePlan(BigDecimal principal, BigDecimal annualRate, int months) { BigDecimal monthlyPayment = calcMonthlyPayment(principal, annualRate, months); BigDecimal monthlyRate = annualRate.divide(new BigDecimal("100"), 8, RoundingMode.HALF_UP) .divide(new BigDecimal("12"), 8, RoundingMode.HALF_UP); BigDecimal remain = principal; List<RepaymentPlan> plans = new ArrayList<>(); for (int i = 1; i <= months; i++) { BigDecimal interest = remain.multiply(monthlyRate) .setScale(2, RoundingMode.HALF_UP); // 最后一期特殊处理:剩余本金全部还完,避免因四舍五入产生最后一期偏差 BigDecimal principalPart = (i == months) ? remain : monthlyPayment.subtract(interest); if (i == months) { interest = monthlyPayment.subtract(principalPart); if (interest.compareTo(BigDecimal.ZERO) < 0) { principalPart = remain; interest = monthlyPayment.subtract(principalPart); } } remain = remain.subtract(principalPart).setScale(2, RoundingMode.HALF_UP); RepaymentPlan plan = new RepaymentPlan(); plan.setPeriod(i); plan.setPrincipal(principalPart); plan.setInterest(interest); plan.setTotal(principalPart.add(interest)); plan.setStatus(0); plans.add(plan); if (remain.compareTo(BigDecimal.ZERO) <= 0) { break; } } return plans; }这段代码里最重要的逻辑是“最后一期特殊处理”。因为每一期都四舍五入到分,到最后一期时剩余本金和理论月供对不上,必须用 remaining 本金作为最后一期的本金,再反推利息,否则最后一期会差一分钱或者负数。我可以说这是金融系统开发里的血泪经验:四舍五入的位置不同,整张还款计划表都会歪掉。
参数上,月利率保留 8 位小数是为了让每次计算误差控制在 1e-8 级别,最后 setScale(2) 再四舍五入到分。年化利率从页面传入时是按 8.00 这种格式存的,而不是 0.08,所以代码里先除 100 再除 12 的顺序不能换。
4. 用 IDEA + Maven + Tomcat 把源码跑起来:配置清单与参数
4.1 依赖清单与版本组合:这几个 jar 一个都不能少
很多同学拿到源码后在 IDEA 里一跑就报 ClassNotFoundException,十有八九是 Maven 依赖没配全。金融借贷系统这类 JavaWeb 项目,最少需要五类依赖:Servlet API、JSP API、MyBatis、MySQL 驱动、JSTL。如果源码里用了 JSON 返回接口,还要加 Jackson 或 Fastjson。
pom.xml 里核心依赖的写法通常是这样:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>这里有两个关键参数:Servlet API 的 scope 必须是 provided,因为 Tomcat 自带 Servlet 实现,打 war 包时如果再带一份会造成类冲突。MySQL 驱动用 8.x 版本,则对应的连接 URL 要带时区参数,否则启动时连数据库会报时区错误。
如果你的源码不是 Maven 项目,而是把 jar 包放在 WEB-INF/lib 下,那就要检查 lib 目录里有没有 mysql 驱动和 mybatis。这种项目 IDEA 运行前需要手动把 lib 目录标记为 library,否则编译期不报错,运行期才报 NoClassDefFoundError,属于比较磨人的黑匣子问题。
4.2 数据库初始化与 MyBatis 配置:先让 SQL 跑对
拿到一个带数据库脚本的项目,第一件事不是启动 Tomcat,而是用脚本把库建出来。直接执行项目里的 init.sql 时,要确认脚本里有没有 CREATE DATABASE 语句,如果没有,需要手动先建库:
mysql -u root -p < init.sql执行完用 SHOW TABLES 检查核心表是否齐全,包括 user、loan、invest、repayment_plan 这几张。如果缺表,多半是 SQL 脚本没有兼容 MySQL 8.x 的语法,比如旧的 TYPE=InnoDB 写法在 8.0 里已经废弃,要替换成 ENGINE=InnoDB。
MyBatis 的全局配置文件 mybatis-config.xml 里最需要关注的是数据库连接信息:
<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/loan_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments>注意 URL 参数里的 serverTimezone=Asia/Shanghai,如果没有这一项,MySQL 8.x 会抛 CST 时区相关的异常。characterEncoding=utf8 统一了连接的字符集,配合本来 utf8mb4 的数据库表,基本能解决八成的中文乱码问题。另外注意 XML 里 & 符号必须转义成 &,写成单个 & 会在解析 mybatis-config.xml 时报错。
连接密码直接写在配置文件里是毕设项目的普遍做法,但在答辩时可以说“生产环境不会这样做”,这反而成为你理解安全问题的加分项。
4.3 IDEA 运行 JavaWeb 项目的三个关键配置
这个部分是 IDEA 里运行 JavaWeb 项目最容易踩坑的地方,很多同学代码没问题,但启动后 404 或者 500,就是下面三个配置没做对。
第一个关键配置:Project Structure 里的 Artifacts。打开 File → Project Structure → Artifacts,确认项目的打包方式是 Web Application Exploded,并且 Output Layout 里有你的项目编译输出。如果没有手动添加过,直接点加号新建一个 Web Application Exploded,名字用默认即可。这一步错了的话,Tomcat 部署时会报“No artifacts marked”的错。
第二个关键配置:Run Configuration。点击 Edit Configurations,新建一个 Tomcat Server → Local,在 Deployment 选项卡里点加号,选择 Artifact,并设置 Application context。这个 Application context 决定你访问路径的前缀,如果项目标题是 JavaWeb 金融借贷系统,context 可以设为 /loan。设完之后启动日志里会打印出完整的访问地址,通常是 http://localhost:8080/loan/。
第三个关键配置:Tomcat 版本与 JRE 的匹配。本地装的 Tomcat 9 要用 JDK 8 或更高,Tomcat 8.5 用 JDK 8 最稳。如果你的项目用了 javax.servlet 的注解配置(比如 @WebServlet),就必须用支持 Servlet 3.0 的 Tomcat 7 以上,老项目里用 Tomcat 6 则完全跑不了注解。检查方式很简单,启动时打开 Tomcat 的日志不要看 IDEA 的 console 里有没有红色异常,最主要的是看 URL 访问是否 404。
把这三点都确认完之后,启动项目看日志里出现“Server startup”基本就成功了。如果还有 404,优先看控制台原本 Tomcat 自己打印的部署路径,确认 context 对不对,再去统一检查访问路径是否漏掉了 /loan 前缀。
5. 避坑:金融借贷系统最常见的 5 个翻车点与排查方法
5.1 中文乱码:从 URL 到数据库全链路排查
现象:页面上输入中文用户名,点提交后数据库里存的是??,或者控制台打印出来的 SQL 里中文直接变乱码。
原因:乱码是字符集在各环节不一致造成的。通常有三个节点:Tomcat 默认的 URL 解码字符集不是 UTF-8、前端 JSP 页面编码不是 UTF-8、MySQL 连接串里没指定 characterEncoding。
解决:前端每个 JSP 页面顶部加上<%@ page contentType="text/html;charset=UTF-8" %>,这个必须放在页面最上面;服务端在过滤器里强制设置 request 和 response 的编码。关键点在 Tomcat 的 server.xml 里对 Connector 节点加 URIEncoding="UTF-8",这样 GET 请求的中文参数才不会乱。数据库连接串里加上 characterEncoding=utf8,三层统一后乱码基本消失。
5.2 金额用 double 存储:一分钱之差打爆还款表
现象:投了 1000 元,交易明细里变成 999.999999999,或者等额本息算出来的最后一期本金差几分钱。
原因:float 和 double 是二进制浮点数,无法精确表示 0.1 这种十进制小数。只要用 double 做过一次除法或乘法,误差就会累积,而这个误差在金融系统里是致命的。
解决:Java 层所有金额相关字段全部用 BigDecimal,数据库层金额字段全部用 DECIMAL,JSON 返回给前端时把 BigDecimal 序列化成字符串而不是数字。接口入参也不要直接接收 double,而是先接字符串再转 BigDecimal。处理规则就一条:计算过程中任何一步都不放开 BigDecimal,除法必须显式指定精度和舍入模式。
5.3 并发下重复放款:两个人同时抢最后一笔额度
现象:一个标的募集总额 10000,还剩 1000 可投。用户 A 和用户 B 同时点了投资 1000,结果两个请求都成功,数据库里募集金额变成 11000,超出了标的总金额。
原因:Service 层先查剩余额度,判断额度足够,再执行插入。两个请求并发时,查询-判断-插入这三步不是原子的,两个请求都通过了判断。
解决:不能只靠 synchronized 关键字,因为 Java 层的锁在应用部署多个实例时没有效果。正确做法是把可用余额的判断落到 SQL 的原子更新里:
UPDATE loan SET raised_amount = raised_amount + #{amount} WHERE id = #{loanId} AND raised_amount + #{amount} <= amount AND status = 1执行后如果影响行数为 0,说明额度已经被抢光或者状态已经变了,此时必须在事务里回滚,并给用户提示“剩余额度不足”。
5.4 SQL 注入:用户名输入一个引号直接查全表
现象:登录页用户名输入' or 1=1 --,密码随便填,居然登录成功了。或者搜索借款标题时输入单引号,页面报 SQL 语法错误。
原因:DAO 层直接用字符串拼接 SQL,参数没有经过任何转义,用户输入被当作 SQL 代码执行。
解决:MyBatis 的#{}是预编译参数占位符,可以安全处理值;恰恰是${}会把参数直接拼接进 SQL,只适合表名、列名这类无法预编译的标识符。如果在项目里搜到${,优先检查这些位置是不是被写成了值传递。JDBC 里用 PreparedStatement 的 setString 替代 Statement,这也是老生常谈但必须检查的点。金融系统里任何 SQL 注入漏洞都会被导师直接抓住。
5.5 会话过期后页面白屏:跳转不到登录页
现象:把系统挂在那里十分钟再操作,点击菜单后没有跳转到登录页,而是出现一个空白页面或者 500 状态码,需要手动重新启动项目才能恢复。
原因:session 过期后,过滤器里直接重定向到 login.jsp,但此时 request 里的 session 已经失效,JSP 页面里如果用了 EL 表达式或 JSTL 标签去访问 session 属性,就会抛异常。另一个可能的原因是过滤器拦截了静态资源,把 css、js 文件都拦下来重定向了。
解决:过滤器里做两件事:一是对静态资源和登录相关的路径放行,比如/assets/*、/login.jsp、/user/login直接放行;二是重定向之前判断是否是 ajax 请求,如果是 ajax 请求,返回 JSON 状态码而不是重定向页面,否则前端拿到的是一段 HTML,解析 JSON 直接报错。
处理方式如下:
if (user == null) { if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期\"}"); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } return; }这块代码解决了前端主页面的 ajax 轮询逻辑,也会遇到查账、投标这些接口返回登录页 HTML 导致解析失败的问题。加上这层判断之后,用户重新登录即可,不用重启服务。
6. 一个值得做的进阶改造:把还款方式做成策略 + 金额校验
如果你手上源码的还款方式是写死的等额本息,我可以建议你做一个不复杂但效果很明显的改造:把还款方式抽成策略接口,然后分别实现等额本息和先息后本两个策略。以后新加一种还款方式,只需要新增实现类,不需要改动原有业务代码。
接口定义非常直观:
public interface RepayStrategy { /** * 生成还款计划 */ List<RepaymentPlan> generatePlan(BigDecimal principal, BigDecimal annualRate, int months); }等额本息策略把前面 3.3 节的计算代码搬进来,先息后本策略单独实现一个类,逻辑是前 n-1 期只还利息,最后一期还本金加利息。然后写一个工厂类RepayStrategyFactory,根据 loan 表里的 repayMethod 字段返回对应的策略:
public class RepayStrategyFactory { public static RepayStrategy getStrategy(Integer method) { if (method == null || method == 0) { return new EqualInstallmentStrategy(); } else if (method == 1) { return new InterestFirstStrategy(); } throw new IllegalArgumentException("不支持的还款方式: " + method); } }改完之后,LoanService 里原来生成还款计划的代码就变成一行:List<RepaymentPlan> plans = RepayStrategyFactory.getStrategy(loan.getRepayMethod()).generatePlan(...)。答辩时你可以明确地说,这是为了应对业务规则变化,新增还款方式时不需要修改已有逻辑,这叫开放封闭原则。
做完改造之后,有一个验证方法我一直认为是金融系统里最值得花时间做的:手工对照验算。拿一张借款 10000 元、年化 12%、期限 12 个月的等额本息标,先自己用公式算出每月应还金额,再跑一遍接口,逐期比对。本金、利息、总额三项各差一分钱都算不过。把验算结果做成一张表格放进毕业设计文档里,比写一万字的功能描述都有说服力。
还有一个字段校验习惯我会强烈建议你补上:金额和利率参数的服务端二次校验。前端页面可以限制输入框是 number,但直接发 HTTP 请求很容易绕过前端验证,比如负数的投资金额就能让余额变为负数。在 Service 层对核心参数做非空、大于 0、小数位不超过两位的检查,是金融系统保命的最后一道防线,而且代码量很小,每三行就能挡住一个事故。
我自己的习惯是拿到任何项目量产之前,先跑一遍异常测试,比如 session 过期后调投资接口、投资人投资自己发布的标、同时两个请求投同一个标的最后一份额度。能扛过这几类的系统,答辩基本不会出大事故。做毕设时不要只满足于“能跑”,金融类系统必须在业务边界处给出可靠的提示。
最后想说的是,这类题目最容易翻车的其实是心态:觉得源码在手就万事大吉,结果连等额本息公式都讲不清。希望这篇笔记帮到你,少走一点弯路。
本文还有配套的精品资源,点击获取