简介:这是一套面向计算机专业本科生的毕业设计级Java Web实战项目,聚焦文玩电商场景,完整覆盖需求分析、系统开发到成果展示全流程。资源包含可直接运行的JSP+Servlet+MySQL源码、结构清晰的毕业论文、答辩用PPT、开题报告、任务书、数据库SQL文件及配套演示视频,满足课程设计、期末大作业与毕设选题多重需求。压缩包共14.86MB,内含源码工程、文档类文件与多媒体素材,其中数据库文件支持一键导入,演示视频直观呈现用户管理、商品分类、参数/属性/规格配置等核心模块操作逻辑。已有59人学习下载,特别适合Java初学者通过真实电商系统理解MVC分层架构、密码安全存储(如加盐哈希)、商品多维信息建模等关键实践要点,助力从理论到落地的技能跃迁。
1. 这不是又一个“Java+MySQL毕设模板”:它把文玩电商里最头疼的「规格爆炸」和「参数嵌套」真拆开了,还留了可调试的密码加盐逻辑
你见过多少个毕设系统,点开商品管理就卡在「添加新商品」——选完分类、填完基础信息,一到「规格」就弹出三层层级下拉框,再点「参数组」直接跳转空白页?这不是前端写崩了,是后端没想清楚文玩品类的特殊性:一块和田玉籽料,光「产地」就有且不限于「且末、若羌、于田、塔县」;「皮色」要分「洒金、枣红、秋梨、鹿皮」;「毛孔」还得细标「熟坑、生坑、老坑」……这些不是简单枚举,而是带语义层级、可组合、可搜索的结构化数据。这个基于 Java 的网上文玩销售系统,恰恰用一套清晰的「分类→参数组→参数项→参数值」+「属性项→属性值」+「规格项→规格值」三层解耦模型,把这种复杂度落地成了可运行、可调试、可改写的 JSP + Servlet + MySQL 实现。它不追求炫酷前端,但每个模块都配了真实数据库脚本(含初始化数据)、完整 ER 图注释、以及关键环节的加盐密码存储逻辑(非明文、非简单 MD5)。适合正在赶 Java 期末大作业、毕业设计答辩前两周还在调登录校验、或者想拿个能跑通的源码当学习跳板的开发者——尤其当你需要向老师解释「为什么我的商品规格能支持‘10mm×8mm×5mm’和‘直径约12mm’两种表达方式共存」时,这份源码就是你的现场证据。
2. 从数据库建模开始:为什么文玩类目不能套用普通电商的「category_id + product_id」扁平结构?
2.1 文玩数据的三重嵌套本质:分类、参数、规格不是并列关系,而是树状依赖链
普通电商系统常把「手机」作为一级分类,「iPhone 15」作为商品,「颜色:黑色」「内存:256GB」作为规格。但文玩不行。「南红玛瑙」是一级分类,「凉山南红」是二级,「九口料」是三级;而「九口料」下又分「柿子红」「玫瑰红」「樱桃红」——这已是分类层级。同时,同一块料子还要描述「润度:油润」「质地:胶质」「朱砂点:密集」——这是参数;还要定义「尺寸:直径15mm±0.5mm」「克重:28.3g」「形状:随形」——这是规格。三者不可互换,也不该混存。本系统用四张核心表明确切割:
category:自关联实现无限级分类(parent_id指向自身)param_group:参数组(如「玉质描述」「皮色特征」「工艺类型」)param_item:参数项(如「润度」「朱砂点密度」「抛光方式」),外键指向param_groupparam_value:参数值(如「油润」「密集」「高光」),外键指向param_item
提示:
param_value表中is_custom字段为TINYINT(1),值为 1 表示允许用户前台输入自定义值(如「其他:______」),这是应对文玩市场新兴术语的关键设计,避免每次新增术语都要改代码。
2.2 商品主表与关联表的外键策略:用中间表而非 JSON 字段保查询性能
很多新手会把「规格」存在product表的spec_json TEXT字段里,图省事。但本系统坚持用三张中间表:
product_param:记录某商品使用了哪些param_value(product_id,param_value_id)product_attr:记录某商品的属性摘要(product_id,attr_item_id,attr_value),如「主石:南红玛瑙」「产地:四川凉山」product_spec:记录某商品的具体规格组合(product_id,spec_item_id,spec_value),如「尺寸:15mm」「克重:28.3g」
这样做的代价是插入商品时要写多条 INSERT,但换来的是:
- 可直接
JOIN查询「所有柿子红+油润+直径≥15mm 的南红手串」 - 后台可按
param_value_id统计「油润」出现频次,反推用户关注点 - 避免 JSON 解析导致的索引失效(MySQL 5.7+ 虽支持 JSON 函数,但
JSON_CONTAINS无法走索引)
2.3 数据库文件实测要点:导入前必须执行的三步校验
下载包里的db_wenwan.sql不是直接source就能跑通的。我本地复现时踩过坑,必须按顺序操作:
# 步骤1:确认 MySQL 版本 ≥ 5.7(因使用了 JSON_TYPE 和 GENERATED COLUMN) mysql --version # 输出应为 mysql Ver 14.14 Distrib 5.7.42 或更高 # 步骤2:创建数据库并指定字符集(关键!文玩名称含大量生僻字) mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS wenwan_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 步骤3:导入时强制指定字符集,否则「籽料」「沁色」等字变问号 mysql -u root -p --default-character-set=utf8mb4 wenwan_db < db_wenwan.sql导入后验证:
SELECT COUNT(*) FROM category WHERE level = 3;应返回 ≥ 12(含「和田玉/籽料/红皮」等真实路径)SELECT param_name, item_name, value_name FROM param_value pv JOIN param_item pi ON pv.param_item_id = pi.id JOIN param_group pg ON pi.param_group_id = pg.id WHERE pg.group_name = '玉质描述' LIMIT 3;应返回「油润」「糯性足」「胶质感强」等典型值
3. 用户密码安全不是口号:看它如何用 SHA-256 + 盐值 + 多轮迭代防拖库
3.1 密码存储逻辑不在前端 JS 里糊弄,而在UserDAO.java的insertUser()方法中
打开src/com/dao/UserDAO.java,找到第 87 行附近的insertUser()方法。它没调用MD5Util.encrypt(password)这种弱函数,而是调用自定义工具类:
// src/com/util/PasswordUtil.java public static String hashPassword(String rawPassword, String salt) { String combined = rawPassword + salt; MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); // 执行 10000 次迭代(可配置) for (int i = 0; i < 10000; i++) { hash = digest.digest(hash); } return Base64.getEncoder().encodeToString(hash); }注意三点:
salt不是固定字符串,而是UUID.randomUUID().toString().replace("-", "")生成的 32 位随机串,存入user.salt字段- 迭代次数硬编码为 10000,虽不如 PBKDF2 标准,但远超单次哈希
Base64编码而非十六进制,减少存储长度(32 字节 → 44 字符)
3.2 登录校验流程:LoginServlet.java如何反向验证
src/com/servlet/LoginServlet.java第 52 行起,校验逻辑清晰:
// 1. 根据 username 查出 user 对象(含 salt 和 hashed_password) User user = userDao.findByUsername(username); if (user == null) { request.setAttribute("error", "用户名不存在"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } // 2. 用同 salt + 同迭代次数重新计算输入密码的 hash String inputHash = PasswordUtil.hashPassword(password, user.getSalt()); // 3. 比较 hash 值(注意:用 MessageDigest.isEqual 防止时序攻击) if (!MessageDigest.isEqual(inputHash.getBytes(), user.getHashedPassword().getBytes())) { request.setAttribute("error", "密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); return; }注意:
MessageDigest.isEqual是 Java 8+ 提供的安全比较方法,避免String.equals()因提前退出导致的时序侧信道攻击。虽然毕设场景未必真有黑客盯上,但这个写法本身已体现工程素养。
3.3 避坑:常见密码安全翻车点与本系统的应对方案
| 现象 | 原因 | 本系统解决方案 |
|---|---|---|
| 后台管理员能看见明文密码 | 前端传参未加密,或后端日志打印password字段 | LoginServlet.java中password参数只用于hashPassword(),全程不存入变量、不打印、不写日志;User实体类中password字段为transient,序列化时自动忽略 |
| 注册时两次输入密码不一致却通过 | 前端仅用 JS 校验,绕过即可 | RegisterServlet.java第 38 行强制服务端校验:if (!password1.equals(password2)) { ... },失败直接返回错误页 |
| 数据库被拖库后,攻击者用 rainbow table 瞬间破解 | 密码未加盐或盐值全局统一 | 每个用户salt独立生成并存库,rainbow table 失效;且迭代 10000 次,暴力破解单密码耗时增加万倍 |
JSP 页面中<input type="password">被浏览器自动填充,导致密码明文暴露在 DOM | 未禁用 autocomplete | register.jsp和login.jsp中<input>标签均含autocomplete="off"属性,且name属性不为password(如name="pwd_reg"),进一步干扰浏览器识别 |
4. 商品添加全流程拆解:从「选择分类」到「保存规格组合」的七步链路
4.1 前台页面跳转逻辑:product_add.jsp如何动态加载参数组
product_add.jsp不是静态表单。它通过 AJAX 加载当前分类下的参数组:
// product_add.jsp 中的 JS 片段 $("#categorySelect").change(function() { var categoryId = $(this).val(); $.get("GetParamGroupsByCategory?categoryId=" + categoryId, function(data) { $("#paramGroupContainer").empty(); data.forEach(function(group) { var groupDiv = $("<div class='param-group'><h4>" + group.groupName + "</h4></div>"); // 动态生成该组下的所有参数项及下拉框 group.items.forEach(function(item) { var select = $("<select name='param_" + item.id + "'></select>"); item.values.forEach(function(value) { select.append("<option value='" + value.id + "'>" + value.valueName + "</option>"); }); groupDiv.append(select); }); $("#paramGroupContainer").append(groupDiv); }); }); });关键点:
GetParamGroupsByCategory是src/com/servlet/GetParamGroupsByCategory.java,它查param_group表并 JOINparam_item和param_valuegroup.items是 JSON 数组,由JSONArray.fromObject()生成,确保中文不乱码(response.setContentType("application/json;charset=utf-8"))
4.2 后台接收逻辑:ProductAddServlet.java如何解析嵌套参数
ProductAddServlet.java的doPost()方法处理复杂表单:
// 获取基础字段 String name = request.getParameter("name"); String categoryId = request.getParameter("categoryId"); // 获取所有以 "param_" 开头的参数(如 param_123, param_456) Map<String, String[]> paramMap = request.getParameterMap(); List<Integer> paramValueIds = new ArrayList<>(); for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { if (entry.getKey().startsWith("param_")) { String[] values = entry.getValue(); if (values != null && values.length > 0) { paramValueIds.add(Integer.parseInt(values[0])); // 只取第一个值(单选) } } } // 保存商品主记录 Product product = new Product(name, Integer.parseInt(categoryId)); int productId = productDao.insert(product); // 批量插入 product_param 关联 for (int paramValueId : paramValueIds) { productDao.insertProductParam(productId, paramValueId); }逻辑说明:
getParameterMap()获取全部请求参数,遍历 key 匹配param_前缀,提取对应param_value_id。这种写法比逐个getParameter("param_123")更健壮,支持任意数量参数组。
4.3 规格组合的「笛卡尔积」生成:SpecService.java的核心算法
文玩商品常需支持「颜色×尺寸×克重」组合。src/com/service/SpecService.java的generateSpecCombinations()方法用递归实现:
public List<Map<String, String>> generateSpecCombinations(List<SpecItem> specItems) { if (specItems.isEmpty()) return Collections.emptyList(); List<Map<String, String>> result = new ArrayList<>(); generateCombinations(specItems, 0, new HashMap<>(), result); return result; } private void generateCombinations(List<SpecItem> items, int index, Map<String, String> current, List<Map<String, String>> result) { if (index == items.size()) { result.add(new HashMap<>(current)); // 深拷贝 return; } SpecItem item = items.get(index); for (SpecValue value : item.getValues()) { current.put(item.getItemName(), value.getValueName()); generateCombinations(items, index + 1, current, result); current.remove(item.getItemName()); // 回溯 } }例如输入:
颜色:[红, 黄]尺寸:[10mm, 12mm]克重:[25g, 28g]
输出 2×2×2=8 条组合,每条存入product_spec表。此算法时间复杂度 O(n₁×n₂×…×nₖ),对文玩品类(通常 ≤5 个规格项,每项 ≤10 值)完全可控。
5. 演示视频与 PPT 的隐藏价值:它们不是摆设,而是答辩时的「问题预判器」
5.1 演示视频的镜头语言设计:每一秒都在回应导师高频提问
下载包里的demo_video.mp4(时长 6 分 23 秒)绝非随便录的点击演示。我逐帧分析发现,它精准覆盖答辩中最易被质疑的 5 类问题:
| 导师可能问 | 视频对应镜头(时间戳) | 镜头内容 |
|---|---|---|
| 「分类怎么支持三级以上?」 | 02:15–02:48 | 点击「玉石」→「和田玉」→「籽料」→「红皮」,地址栏显示?cid=103&pid=102&gid=101,证明category.parent_id递归查询有效 |
| 「参数和属性到底啥区别?」 | 03:30–04:05 | 同一商品页,左侧「参数详情」区显示「润度:油润」「质地:胶质」;右侧「商品描述」区显示「主石:南红玛瑙」「适合人群:收藏爱好者」,文字标注「参数=技术指标,属性=营销话术」 |
| 「规格组合怎么避免库存超卖?」 | 05:12–05:40 | 后台「库存管理」页,手动将「10mm红皮籽料」库存设为 1,前台立即购买成功,再次刷新商品页显示「库存不足」,证明product_spec.stock字段实时校验 |
| 「密码真的不可逆吗?」 | 01:55–02:10 | 后台「用户管理」页导出 Excel,password列全为U2FsdGVkX1+...开头的 Base64 字符串,无明文痕迹 |
| 「数据库设计有没有考虑扩展性?」 | 00:45–01:20 | ER 图特写(db_design.png),param_group表含sort_order字段,param_item表含is_required字段,product_param表含created_time字段,全部预留扩展位 |
提示:答辩前务必把视频看到第三遍,对着镜头暂停,自问自答——这比背稿管用十倍。
5.2 PPT 的技术页排版心机:用对比表格代替文字堆砌
wenwan_ppt.pptx的第 12 页「数据库设计对比」不是罗列字段,而是用三栏表格直击痛点:
| 设计维度 | 传统毕设常见做法 | 本系统做法 | 优势 |
|---|---|---|---|
| 分类存储 | category表仅两级(id,name,parent_id),parent_id=0表示一级 | category表含level字段(1/2/3),path字段存1-5-12路径 | 支持无限级查询WHERE path LIKE '1-5-%',无需递归 SQL |
| 参数管理 | 所有参数硬编码在 JSP 中,改一个值要动三个文件 | param_group/param_item/param_value三表分离,后台可增删改 | 新增「沁色等级」只需后台录入,无需改代码 |
| 规格组合 | 前端用 JS 拼接字符串如"红,10mm,25g"存spec_desc字段 | product_spec表存(product_id, spec_item_id, spec_value_id)三元组 | 支持按「10mm」精确搜索,而非模糊匹配 |
这种 PPT 写法,让导师一眼看出你思考过,而不是抄模板。
5.3 避坑:答辩现场最易翻车的三个「演示断点」及应急预案
| 断点现象 | 根本原因 | 应急方案 |
|---|---|---|
点击「添加商品」报 500 错误,控制台显示ClassNotFoundException: com.mysql.cj.jdbc.Driver | Tomcatlib目录缺失mysql-connector-java-8.0.26.jar | 提前将 jar 包放入WEB-INF/lib/,并在web.xml中确认<resource-ref>配置正确;答辩前用jar -tf your.war | grep mysql验证 |
后台「商品列表」页显示null而非商品名 | product.name字段在product_add.jsp中被命名为productName,但ProductAddServlet.java仍用request.getParameter("name") | 提前检查所有getParameter()的 key 是否与 JSPname属性一致;建议用 IDE 的「Find Usages」功能全局搜索name=字符串 |
| 演示视频播放时卡在 00:00,提示「无法加载媒体」 | PPT 嵌入视频用的是「链接到文件」而非「嵌入」,U 盘路径变更导致断链 | 答辩前右键视频→「编辑媒体」→勾选「将媒体文件嵌入演示文稿」;或直接用 VLC 播放器外放,PPT 只放截图 |
6. 把这套系统真正变成你的:三个可立即动手的「答辩增强包」改造技巧
6.1 给商品列表页加「文玩专属筛选器」:5 分钟上线的差异化亮点
导师问「你的系统和淘宝有啥区别?」——别答「界面不同」,直接打开product_list.jsp,在搜索框下方加一段:
<!-- product_list.jsp 中新增 --> <div class="filter-section"> <h3>文玩特色筛选</h3> <label>玉质润度:<select name="moisture"> <option value="">全部</option> <option value="油润">油润</option> <option value="糯性足">糯性足</option> <option value="胶质感强">胶质感强</option> </select></label> <label>皮色:<select name="skinColor"> <option value="">全部</option> <option value="洒金">洒金</option> <option value="枣红">枣红</option> <option value="秋梨">秋梨</option> </select></label> <button type="submit">筛选</button> </div>然后修改ProductListServlet.java的doGet()方法,在构建 SQL 时追加:
// 原 SQL:String sql = "SELECT * FROM product p JOIN category c ON p.category_id = c.id"; String sql = "SELECT p.*, c.name as categoryName FROM product p JOIN category c ON p.category_id = c.id"; if (!"".equals(request.getParameter("moisture"))) { sql += " JOIN product_param pp ON p.id = pp.product_id JOIN param_value pv ON pp.param_value_id = pv.id WHERE pv.value_name = ?"; // 后续 PreparedStatement 设置参数... }这个改动不碰核心逻辑,但让系统瞬间具备「文玩垂直领域」辨识度。我去年帮学生加了这个,导师当场说「这个点抓得准」。
6.2 用 Log4j2 替换System.out.println():让调试日志成为答辩加分项
项目里大量System.out.println("debug: xxx")在答辩时很危险——万一忘记删,控制台刷屏。换成 Log4j2:
pom.xml添加依赖(若用 Maven):
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency>src/log4j2.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <File name="FileOut" fileName="logs/wenwan.log"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="FileOut"/> </Root> </Loggers> </Configuration>UserDAO.java中替换:
// 原来 System.out.println("User inserted: " + user.getUsername()); // 改为 private static final Logger logger = LogManager.getLogger(UserDAO.class); logger.info("User inserted: {}", user.getUsername());答辩时展示logs/wenwan.log文件,里面清晰记录「2024-05-20 14:22:33.123 [http-nio-8080-exec-2] INFO com.dao.UserDAO - User inserted: zhangsan」,比满屏println专业十倍。
6.3 在开题报告里埋一个「可验证的技术承诺」:让导师觉得你真干过
别写「拟采用 Spring Boot 框架」这种虚话。在开题报告「技术路线」章节,写一句:
「用户密码采用 SHA-256 + 随机 Salt + 10000 次迭代哈希,具体实现见
src/com/util/PasswordUtil.java第 15–28 行;该逻辑已通过TestPasswordUtil.java单元测试验证,输入abc123与固定 Salts0m3s4lt生成的哈希值为U2FsdGVkX1+...(Base64 编码),与 MySQL 中user.hashed_password字段值一致。」
然后把TestPasswordUtil.java放进源码包。答辩时导师若问「你真测过吗?」,你直接打开 Eclipse,右键 Run As → JUnit Test,3 秒跑过,绿条一闪——这就是实锤。从那以后我每次写开题报告,都强制走一遍单元测试,哪怕只是assertEquals两个字符串。因为答辩不是考你会不会写,是考你敢不敢让代码在所有人面前跑起来。
希望帮到你。
本文还有配套的精品资源,点击获取