拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM客户资料管理系统毕设实战:框架整合、数据库脚本与避坑指南

SSM客户资料管理系统毕设实战:框架整合、数据库脚本与避坑指南

简介:基于SSM框架的客户资料管理系统项目源码包,是一套面向计算机相关专业毕设学生和Java学习者的完整课程设计资源,可直接用于毕业设计或项目实战训练。整个压缩包内共有3个文件,其中源码包内含完整前后端代码,sql脚本用于快速初始化MySQL数据库,txt项目说明对运行环境和部署步骤做了必要提示,包体大小约25.18MB。系统采用Spring、SpringMVC、MyBatis框架,前端用JSP实现,可在JDK、Eclipse、Tomcat环境下运行,围绕管理员、客户经理、营销主管三类角色提供员工信息、产品信息、客户信息、服务信息、交易信息、基础信息查询等模块化功能,界面清晰,操作直观。已有1112人浏览学习,适合需要快速搭建CRM系统原型、理解SSM整合流程或准备毕设答辩演示的读者。

1. 用 SSM 做客户资料管理系统:毕设选题为什么依旧能打

临近毕设的节点,「客户资料管理系统」总被当成老掉牙的题目,但正是这种看似普通的题目,最能检验一个人能不能把一个完整项目从数据库脚本跑到浏览器页面。SSM 这套组合——Spring 管对象和事务、SpringMVC 管请求路由、MyBatis 管 SQL 映射——到今天仍然是中小型管理系统的主流后端结构。这个题目能解决的是:从零搭起一套可演示的客户档案增删改查,附带可重复执行的数据库脚本,让你在答辩时既拿得出源码,也讲得清配置。适合 Java 基础一般、需要快速交付完整项目的同学,也适合想把 Spring 容器和 MyBatis 原理讲明白的技术型选手。这篇笔记就按选型、建库、整合、避坑、加分的顺序,把整套落地路径摊开讲。

2. SSM 客户资料管理系统的框架选型:Spring、SpringMVC、MyBatis 各管哪一块

2.1 为什么客户资料这种业务适合用 SSM 复现

客户资料管理系统的核心是单表为主、外键少、查询条件多:按姓名模糊查、按行业过滤、按客户等级筛选、跟进状态流转。这些需求放到 SSM 里,正好对应三个层面的分工:Spring 容器负责把 Service、Mapper 这些对象组装起来,并通过声明式事务保证「新增客户」这类操作要么成功要么回滚;SpringMVC 把浏览器的表单请求映射到 Java 方法,参数自动绑定到 POJO;MyBatis 则把 SQL 写在 XML 里,动态条件可以按参数拼装,避免写死 SQL。对毕设来说,这种分工还有个额外好处:每个环节都能对应一个运行机制问题,比如「请求是怎么从 JSP 到数据库的」「事务注解为什么有时不生效」,这些是答辩老师最常问的点。

有人会问:现在都用 Spring Boot 了,SSM 是不是过时?我的看法是,Spring Boot 把配置自动完成,项目能跑但讲不透;SSM 虽然 XML 多一些,但每一个配置都能在启动过程里找到对应位置,反而更适合拿来展示基本功。如果你目标是快速出活,Spring Boot 更香;但标题既然锁在 SSM,这篇笔记就把 SSM 的整条链路讲清楚,让你在「能跑」之外还能讲明白。

2.2 一次「新增客户」请求是如何穿过 SSM 三层的

别急着写代码,先在脑子里把这条链路立住。用户在 JSP 页面填好表单点提交,浏览器发一个 POST 请求到/customer/add。请求先进 Tomcat,送到 DispatcherServlet——SpringMVC 的前端控制器。DispatcherServlet 根据 URL 找到 CustomerController 里标注了@PostMapping("/add")的方法,并把表单字段(customerName、contactPhone、email…)自动封装成 Customer 对象。Controller 不直接碰数据库,它调用 Service 接口,Service 实现类加上@Transactional后,通过 CustomerMapper 接口去操作 t_customer 表。MyBatis 在这里做的事是:读取 resources/mapper 下的 XML 映射文件,把接口方法名和 SQL 对应起来,执行后把结果集映射回 List。返回时 Controller 返回一个逻辑视图名,视图解析器拼出/WEB-INF/views/customer/list.jsp,Tomcat 渲染完页面回给浏览器。

这条链路里最容易理解错误的是:Controller 和 Service 都是普通的 Java Bean,靠 Spring 容器管理;你不需要用new去创建它们。@Autowired会把容器里已经初始化好的对象注入进来,如果注入失败,启动时就会报NoSuchBeanDefinitionException,这时候先检查包扫描路径是不是覆盖到了对应包。

2.3 依赖坐标与版本搭配:一份能直接翻建项目的 pom.xml

SSM 项目本质上是一个 Maven Web 项目。我一般会固定 Spring 5.2.x、MyBatis 3.5.x、MySQL 8.0.x,这几个版本搭配成熟,网上能找到大量兼容案例。pom.xml 里这些依赖缺一不可,尤其不要漏掉 mybatis-spring,它是 MyBatis 和 Spring 之间的桥梁。

<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.2.20.RELEASE</spring.version> <mybatis.version>3.5.9</mybatis.version> </properties> <dependencies> <!-- Spring MVC 会连带引 Spring 核心容器 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- 事务管理需要 DataSourceTransactionManager --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- MySQL 8 必须用 com.mysql.cj.jdbc.Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <!-- Druid 连接池,参数比默认连接池直观 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>

依赖里需要留意的是scope=provided的 servlet-api:Tomcat 本身已经有 Servlet 容器实现,如果打成 war 包时把这个依赖也带进去,不同版本的 Servlet 类会冲突,启动时可能报NoSuchMethodError。spring-jdbc是事务的基础,mybatis-spring负责把 MyBatis 的 SqlSessionFactory 交到 Spring 容器里,并让 Mapper 接口能被自动注册。JDK 版本用 1.8,Tomcat 用 8.5 或 9,不要用 Tomcat 10,因为 Tomcat 10 的 Jakarta 命名空间和 SSM 这套 javax.* 依赖默认不兼容,那种翻车现场我见过不止一次。

3. 从数据库脚本开始落地:建表、初始化数据与 IDEA 导入脚本的边界坑

3.1 客户资料表的字段设计与规范化

客户资料表的字段设计决定了后续代码量。最怕的就是把一堆备注、地址、联系人堆在一张表里,造出一张几十列的宽表。对一个毕设来说,字段在 8 到 12 列之间最合适,既能覆盖常见需求,又能在答辩时快速讲清每个字段的作用。我推荐的初始化脚本如下:

CREATE DATABASE IF NOT EXISTS customer_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE customer_db; DROP TABLE IF EXISTS t_customer; CREATE TABLE t_customer ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', customer_name VARCHAR(64) NOT NULL COMMENT '客户名称', contact_phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', email VARCHAR(128) DEFAULT NULL COMMENT '邮箱', industry VARCHAR(32) DEFAULT NULL COMMENT '所属行业', level TINYINT NOT NULL DEFAULT 1 COMMENT '客户等级 1~5', follow_status TINYINT NOT NULL DEFAULT 0 COMMENT '跟进状态 0未跟进 1跟进中 2已成交 3已流失', remark VARCHAR(500) DEFAULT NULL COMMENT '备注', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_contact_phone (contact_phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户资料表'; INSERT INTO t_customer (customer_name, contact_phone, email, industry, level, follow_status, remark) VALUES ('测试客户A', '13800138000', 'testa@example.com', '制造', 3, 1, '重点跟进中'), ('测试客户B', '13900139000', 'testb@example.com', '软件', 2, 0, NULL);

几个字段选型理由可以当答辩素材:电话为什么用 VARCHAR 而不是 BIGINT?因为电话号码可能出现分机号、以 0 开头,数字类型会把前导零丢掉;邮箱长度不同,VARCHAR 比 CHAR 省空间;level 用 TINYINT 而不是 VARCHAR 存「高、中、低」,是为了排序方便,MySQL 按数字排序比按中文字符串排序可靠。create_time 由数据库生成,update_time 用ON UPDATE CURRENT_TIMESTAMP自动更新,这样演示「修改客户资料」时,只需要改 update_time 字段就能说明数据在变化,不用手动维护时间。

如果你的 MySQL 版本是 5.6 之前,DATETIME 不支持DEFAULT CURRENT_TIMESTAMP,这种写法会直接语法报错。遇到这种情况,把 create_time 和 update_time 改成 TIMESTAMP 类型,或者直接在 Java 层 new Date() 再 set 进去。既然面向现代部署环境,优先 MySQL 8.0 最省事。

3.2 用 IDEA 执行数据库脚本:先建库,再建表,不要全选直接 Run

拿到项目源码里的数据库脚本文件后,不要上来就选中整个文件按运行,那样一条报错会让后面脚本全部停在原地。正确顺序是:在 IDEA 右侧 Database 面板新建一个 DataSource,选择 MySQL,URL 写jdbc:mysql://localhost:3306/customer_db?useSSL=false&serverTimezone=Asia/Shanghai,用户名密码按本机配置填,先把连接建好。然后右键这个数据源,选择 Open Console,在 Console 里执行CREATE DATABASE IF NOT EXISTS ...,再打开 init_db.sql,只选中建表和插入语句部分执行。

命令行方式也适用,Linux 或 Windows 终端里执行:

mysql -uroot -p < init_db.sql

执行完用这条 SQL 确认表和数据都到位:

SELECT COUNT(*) FROM t_customer;

如果返回的不是 0,说明初始化数据写入成功。这里最容易犯的「小白操作」是脚本开头没有USE customer_db,在数据库 Console 里执行时会把表建到默认的 information_schema 或者某个旧库里去。所以脚本第一段必须显式声明库名,这也是我在项目源码里看到的最常见的交付缺陷。

3.3 数据库脚本导出的几个坑:从「本机能跑」到「对方电脑能跑」

毕设要交付的是整个项目源码 + 数据库脚本,这里的数据库脚本不只是你本机导出的那一份 dump,而是一份能在空环境里重复执行的初始化脚本。IDEA 导出数据库脚本是个高频动作,但直接导出常常会踩到三个边界:

第一,导出的 SQL 文件可能不包含 CREATE DATABASE。你自己的连接本来就在 customer_db 里,导出的表语句都是CREATE TABLE t_customer,对方新建连接时不知道该切到哪个库。解决方法是导出后检查文件头部,没有CREATE DATABASE就手动补上,或者直接用我们上面写的 init_db.sql 作为唯一交付脚本。

第二,编码问题。Windows 上用 IDEA 导出时,如果 File Encoding 是 GBK,导出的 SQL 里中文备注会变成乱码。解决方法是导出前把脚本右下角编码切到 UTF-8,并且导出后重新执行一次,再 SELECT 查一下中文是否正常。数据库脚本里中文字段名尽量避免,表和字段名统一英文,注释可以保留中文,但前提是整个链路编码一致。

第三,约束丢失。只导出数据时,唯一索引、外键这些结构信息不会出现在 INSERT 语句里。所以一定要确认导出选项里选了「Structure + Data」或「Schema with Data」。否则你在本机测试唯一约束没问题,交到对方电脑上插入了重复手机号也不报错,答辩现场老师一看就露馅了。我的建议是:不让 IDE 导出的全量脚本当唯一的交付物,而是维护一份手写的 init_db.sql,里面把结构、约束、初始化数据、注释都控制在自己手里。

4. 把项目跑起来:SSM 整合配置与客户资料管理的最小 CRUD

4.1 包规划与三层结构:Controller、Service、Mapper 各归其位

SSM 项目的第一件事不是写代码,而是把包结构定下来。包结构乱的项目,等配置文件写好之后常常出现「Mapper 扫描不到」「Controller 注入失败」这类玄学问题,其实都是包路径没对上。我习惯这样搭:

src/main/java/com/example/customer/ ├── controller/CustomerController.java ├── service/CustomerService.java ├── service/impl/CustomerServiceImpl.java ├── mapper/CustomerMapper.java └── pojo/Customer.java src/main/resources/ ├── applicationContext.xml ├── spring-mvc.xml ├── mybatis-config.xml ├── jdbc.properties └── mapper/CustomerMapper.xml

注意一个关键点:CustomerMapper.java 接口在com.example.customer.mapper包下,而 CustomerMapper.xml 放在 resources 下的mapper/目录。这种放法能让 XML 在编译后留在 classes 根目录的 mapper 里,MyBatis 用classpath:mapper/*.xml就能扫到。如果你想更规范,可以把 XML 和接口放在同一个包路径下,但那样需要在 pom.xml 里额外配置 resources 包含 xml 文件,否则打包后 XML 丢失。对新手来说,XML 统一放 resources/mapper 是最不容易出错的方案。

POJO 的字段类型要和数据库列对齐:BigDecimal 对应 DECIMAL,String 对应 VARCHAR,Integer 对应 TINYINT,LocalDateTime 对应 DATETIME。如果 Java 侧用 LocalDateTime,MyBatis 3.5.x 无需额外配置就能映射;如果你项目里用的还是 java.util.Date,也问题不大,只是打印日志时没那么直观。

4.2 三份配置文件的职责边界:Spring 容器、SpringMVC 容器和 MyBatis 设置

SSM 里有三份 XML,很多人搞不清为什么要拆开。applicationContext.xml 管全项目共有对象:Service、数据源、事务、MyBatis 的 SqlSessionFactory;spring-mvc.xml 只管 Controller 和请求相关组件;mybatis-config.xml 只放 MyBatis 的全局设置,比如驼峰映射和日志。这样拆的目的是避免 Controller 被 Spring 容器和 SpringMVC 容器重复扫描,重复扫描会导致事务注解失效,这是 SSM 整合的一个典型雷区。

applicationContext.xml 的核心配置:

<!-- 扫描 Service 层实现类,排除 Controller --> <context:component-scan base-package="com.example.customer"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="2"/> <property name="maxActive" value="10"/> </bean> <!-- 把 MyBatis 的 SqlSessionFactory 交给 Spring 管理 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.example.customer.pojo"/> </bean> <!-- 扫描 Mapper 接口,自动生成实现 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.customer.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

typeAliasesPackage的作用是让 XML 里写resultType="Customer"而不是写一大串全限定类名。MapperScannerConfigurer会把接口统一注册进 Spring 容器,之后你在 Controller 里直接@Autowired指定的接口名即可。tx:annotation-driven是@Transactional生效的前提,少了这一行,后面写了事务注解也不会有回滚行为。

spring-mvc.xml 要单独扫描 controller 包,并配置视图解析器:

<context:component-scan base-package="com.example.customer.controller"/> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:default-servlet-handler/>

视图解析器把 Controller 返回的customer/list解析成/WEB-INF/views/customer/list.jsp。mvc:default-servlet-handler保证静态资源如 CSS、JS 能找到 Tomcat 的默认 Servlet,否则样式会全部失效。

mybatis-config.xml 保持精简:

<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration>

mapUnderscoreToCamelCase开启后,数据库列 customer_name 会自动映射到 Java 属性 customerName,不用每个字段写 resultMap。STDOUT_LOGGING会把 SQL 语句和执行结果打印到控制台,调试时看它比看业务日志快得多。

4.3 实现客户新增与列表查询:Controller、Service、Mapper 三层联动

配置齐了,代码可以从最小闭环开始写。先写 CustomerController,只做两件事:接收新增表单、查列表。

@Controller @RequestMapping("/customer") public class CustomerController { @Autowired private CustomerService customerService; @PostMapping("/add") public String add(Customer customer) { customerService.addCustomer(customer); return "redirect:/customer/list"; } @GetMapping("/list") public String list(@RequestParam(defaultValue = "") String customerName, Model model) { model.addAttribute("list", customerService.queryCustomer(customerName)); return "customer/list"; } }

@PostMapping("/add")是 Spring 4.3 之后的组合注解,等价于@RequestMapping(value="/add", method=POST)。方法参数直接写 Customer,SpringMVC 会把 request 里的参数按字段名绑定到对象上,省去手写request.getParameter的重复劳动。新增成功之后用 redirect 重定向到列表页,避免刷新页面时重复提交表单。

Service 接口和实现:

public interface CustomerService { int addCustomer(Customer customer); List<Customer> queryCustomer(String customerName); }
@Service public class CustomerServiceImpl implements CustomerService { @Autowired private CustomerMapper customerMapper; @Override @Transactional public int addCustomer(Customer customer) { return customerMapper.insertCustomer(customer); } @Override public List<Customer> queryCustomer(String customerName) { return customerMapper.selectByCondition(customerName); } }

@Transactional里不写任何参数时,默认对运行时异常回滚。如果你在方法里抛了一个RuntimeException,Spring 会回滚;如果抛的是受检异常,默认不回滚。答辩时这个区别经常被追问,最好记住。

Mapper 接口则要配 XML 才有实际 SQL:

public interface CustomerMapper { int insertCustomer(Customer customer); List<Customer> selectByCondition(@Param("customerName") String customerName); }
<insert id="insertCustomer" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_customer (customer_name, contact_phone, email, industry, level, follow_status, remark) VALUES (#{customerName}, #{contactPhone}, #{email}, #{industry}, #{level}, #{followStatus}, #{remark}) </insert> <select id="selectByCondition" resultType="Customer"> SELECT id, customer_name, contact_phone, email, industry, level, follow_status, remark, create_time, update_time FROM t_customer <where> <if test="customerName != null and customerName != ''"> AND customer_name LIKE CONCAT('%', #{customerName}, '%') </if> </where> ORDER BY update_time DESC </select>

useGeneratedKeys="true"会在执行 INSERT 后把自增主键回填到 Customer 对象的 id 属性,方便在新增完成后立刻拿到新客户 ID。<where>标签会自动处理第一个条件前面的 AND,如果 customerName 为空,它生成的 SQL 不会带 WHERE,整个列表就能查出来。LIKE CONCAT('%', #{customerName}, '%')是参数化查询,没有字符串拼接导致 SQL 注入的风险,这里不要图省事写成'%${customerName}%',后者会把参数拼进 SQL,是明显的注入漏洞,答辩时被点名要扣分的。

5. 毕设答辩与自测避坑:中文乱码、事务失效、部署翻车

5.1 插入到数据库的中文全变成「??」:四级字符集排查链

现象:页面上填的「测试客户」,保存后数据库里显示的是「??」。原因不是某一处编码问题,而是页面响应编码、请求传输编码、JDBC 连接编码、数据库表编码四级里至少有一级不是 UTF-8。解决时按这条链从外到内检查:JSP 顶部加上pageEncoding="UTF-8";web.xml 里配置 CharacterEncodingFilter 强制 request 和 response 都用 UTF-8;JDBC URL 末尾加characterEncoding=utf8;数据库连接和表结构确认是utf8mb4。这四级里漏掉任何一级,中文乱码都会以不同形式冒出来。

具体到 SSM 项目里,最常见的是只配了数据库,没配 Tomcat 层。如果你的请求是 GET,Tomcat 8.5 默认 URI 编码是 UTF-8,问题还不明显;如果是 POST 表单,没有过滤器时 request 默认按 ISO-8859-1 解码,中文必然乱。加入 CharacterEncodingFilter 后,绝大多数乱码都能拦住。

5.2 加了 @Transactional 异常后数据还是写进去了

现象:Service 方法先是customerMapper.insertCustomer(customer),然后手动抛一个RuntimeException,按预期应该回滚,但数据库里客户记录还在。原因通常是三选一:applicationContext.xml 里没有写tx:annotation-driven;或者事务注解写在了接口方法上而实现类里没有,Spring 的注解扫描默认只认实现类;或者同一个类内部调用事务方法,导致事务没经过 Spring 的处理边界。这里最隐蔽的是第三种:类内部调用不会经过 Spring 生成的增强对象,事务注解形同虚设。

解决方法是:确认 Spring 容器配置了事务驱动;把@Transactional写在实现类方法上;需要自调用时,把事务逻辑拆到另一个 Bean 里,或者用AopContext.currentProxy(),不过毕设场景我更建议直接拆类。验证回滚的方式是:在方法末尾故意除以零抛异常,然后在数据库里查这条记录,如果不存在,事务生效。

5.3 部署后列表页能打开,提交表单却 404

现象:直接访问/customer/list能看到页面,但表单提交到/customer/add报 404。原因一:表单里的 action 写成了/customer/add,但项目部署名是customer-manage,实际访问路径应该是http://localhost:8080/customer-manage/customer/add,少了项目上下文路径自然 404。原因二:DispatcherServlet 在 web.xml 里映射成了/*,把所有请求包括 JSP 都拦截下来,转发到视图解析器后找不到对应的 Controller,导致 404 或 405。原因三:表单提交方式写成了 GET,而后台方法是 POST,会报 405。

解决方法是:在 JSP 里统一用${pageContext.request.contextPath}拼请求地址;web.xml 中把 DispatcherServlet 映射路径写为/,而不是/*;提交方式用<form method="post">,不要因为懒而省略。这三条做到后,404 基本能消灭。

5.4 IDEA 导出数据库脚本后再导入,唯一约束丢失

现象:本机执行 init_db.sql 一切正常,但把 IDEA 导出的脚本给对方后,插入重复手机号居然不报错。原因是 IDEA 数据库面板的导出选项默认是只导出数据,结构里的 PRIMARY KEY、UNIQUE KEY 没有一并导出;或者导出的 SQL 里没有切到目标库,把表建到了搞错的地方。解决方法是:导出时选择 Schema with Data,导完打开文本搜一下UNIQUE和CREATE TABLE,确认约束存在。更稳的做法是不要让 IDE 的导出结果替代手写脚本,手写脚本可以做到每条 SQL 都可控,IDE 导出只作为备份。这条经验在我做项目源码交付时帮了很大的忙,研发出机环境下数据库结构不一致,八成都是这个环节出的问题。

5.5 项目源码压缩包发过去后,对方启动失败

现象:你本机能跑,同学导入后却报「Could not resolve javax.servlet-api」或者「Unknown database 'customer_db'」。原因多半是三件事:对方本地 Maven 仓库缺依赖,网络又不通;数据库账号密码不是你写在 jdbc.properties 里的那套;还有可能是对方 JDK 是 17,而你用的是 1.8,导致编译失败。解决方法是:pom.xml 里明确定死版本,不要用变量悬空;jdbc.properties 里用注释标明「改成你自己的库名和密码」;项目里附带一份 README 或者部署说明文档,写明 JDK 8、Tomcat 8.5/9、MySQL 8、Maven 3.6 这几项前置环境。这些东西不会增加太多工作量,但直接决定别人照着你的项目源码能不能复现,毕设评审时对方跑不起来比功能少写两项还难评。

6. 把项目做成能答辩的加分项:客户资料检索的进阶实现与验证技巧

6.1 给列表查询加一个真正可用的组合分页

前面写的是单个条件模糊查询,答辩时如果要加亮点,建议叠加「多条件组合查询 + 分页」,这是客户资料管理系统最贴近真实场景的需求。Controller 接收 pageNum 和 pageSize,把参数传给 Mapper,SQL 里用LIMIT #{offset}, #{size}做物理分页:

<select id="selectPage" resultType="Customer"> SELECT id, customer_name, contact_phone, email, industry, level, follow_status, create_time, update_time FROM t_customer <where> <if test="customerName != null and customerName != ''"> AND customer_name LIKE CONCAT('%', #{customerName}, '%') </if> <if test="industry != null and industry != ''"> AND industry = #{industry} </if> <if test="level != null"> AND level = #{level} </if> </where> ORDER BY update_time DESC LIMIT #{offset}, #{size} </select>

分页参数的计算公式:offset = (pageNum - 1) * pageSize,pageNum 从 1 开始。为什么不直接用 PageHelper?PageHelper 在 SSM 里也能用,但它的分页拦截器对老项目有时候会吞掉 count 查询,出现页面上显示总条数不对。手写 LIMIT 的代码量不大,更适合毕设这种需要把原理讲清楚的项目。

6.2 自测时别只点页面,用命令和 SQL 快速做回归

页面能点通不代表边界条件没问题。我建议答辩前做一轮命令级验证,趁早暴露问题。启动项目后用 curl 发一次新增:

curl -X POST "http://localhost:8080/customer/add" \ -d "customerName=测试客户A&contactPhone=13800138000&level=3"

再用一条 SQL 确认插入结果:

SELECT id, customer_name, contact_phone, level FROM t_customer WHERE contact_phone = '13800138000';

如果返回一条记录,说明穿过了 DispatcherServlet、Controller、Service、Mapper 全链路。接着再发一次同样的请求,看是否因为唯一约束报错,这个报错能证明数据库脚本里的 UNIQUE KEY 没有丢。最后验证事务回滚:在 Service 的 addCustomer 方法里临时加一行throw new RuntimeException("rollback check");,重新请求后查库,如果查不到新记录,说明事务配置是通的。验完把这行异常删掉即可。

我自己的习惯是:每次拿到一套新的项目源码,先按「建库 -> 导数据 -> 启动 -> 走一遍 CRUD -> 打一个 war 包」的顺序跑通,再去想功能扩展。客户资料管理系统这类毕设,不需要做得多花哨,把数据库脚本、事务回滚、组合查询这三个点做实,已经比大多数「只能点通页面」的项目强出不少。别把客户资料表设计成无底洞,字段越堆越多,最后自己维护不过来,答辩被问细节时反而容易翻车。希望帮到你。

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

返回列表