简介这是一套面向计算机专业本科生的毕业设计级Java Web实战项目聚焦文玩电商场景完整覆盖需求分析、系统开发到成果展示全流程。资源包含可直接运行的JSPServletMySQL源码、结构清晰的毕业论文、答辩用PPT、开题报告、任务书、数据库SQL文件及配套演示视频满足课程设计、期末大作业与毕设选题多重需求。压缩包共14.86MB内含源码工程、文档类文件与多媒体素材其中数据库文件支持一键导入演示视频直观呈现用户管理、商品分类、参数/属性/规格配置等核心模块操作逻辑。已有59人学习下载特别适合Java初学者通过真实电商系统理解MVC分层架构、密码安全存储如加盐哈希、商品多维信息建模等关键实践要点助力从理论到落地的技能跃迁。1. 这不是又一个“JavaMySQL毕设模板”它把文玩电商里最头疼的「规格爆炸」和「参数嵌套」真拆开了还留了可调试的密码加盐逻辑你见过多少个毕设系统点开商品管理就卡在「添加新商品」——选完分类、填完基础信息一到「规格」就弹出三层层级下拉框再点「参数组」直接跳转空白页这不是前端写崩了是后端没想清楚文玩品类的特殊性一块和田玉籽料光「产地」就有且不限于「且末、若羌、于田、塔县」「皮色」要分「洒金、枣红、秋梨、鹿皮」「毛孔」还得细标「熟坑、生坑、老坑」……这些不是简单枚举而是带语义层级、可组合、可搜索的结构化数据。这个基于 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_valueproduct_id,param_value_idproduct_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-setutf8mb4 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 typepassword被浏览器自动填充导致密码明文暴露在 DOM未禁用 autocompleteregister.jsp和login.jsp中input标签均含autocompleteoff属性且name属性不为password如namepwd_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 classparam-grouph4 group.groupName /h4/div); // 动态生成该组下的所有参数项及下拉框 group.items.forEach(function(item) { var select $(select nameparam_ 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;charsetutf-8)4.2 后台接收逻辑ProductAddServlet.java如何解析嵌套参数ProductAddServlet.java的doPost()方法处理复杂表单// 获取基础字段 String name request.getParameter(name); String categoryId request.getParameter(categoryId); // 获取所有以 param_ 开头的参数如 param_123, param_456 MapString, String[] paramMap request.getParameterMap(); ListInteger paramValueIds new ArrayList(); for (Map.EntryString, 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 ListMapString, String generateSpecCombinations(ListSpecItem specItems) { if (specItems.isEmpty()) return Collections.emptyList(); ListMapString, String result new ArrayList(); generateCombinations(specItems, 0, new HashMap(), result); return result; } private void generateCombinations(ListSpecItem items, int index, MapString, String current, ListMapString, 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×28 条组合每条存入product_spec表。此算法时间复杂度 O(n₁×n₂×…×nₖ)对文玩品类通常 ≤5 个规格项每项 ≤10 值完全可控。5. 演示视频与 PPT 的隐藏价值它们不是摆设而是答辩时的「问题预判器」5.1 演示视频的镜头语言设计每一秒都在回应导师高频提问下载包里的demo_video.mp4时长 6 分 23 秒绝非随便录的点击演示。我逐帧分析发现它精准覆盖答辩中最易被质疑的 5 类问题导师可能问视频对应镜头时间戳镜头内容「分类怎么支持三级以上」02:15–02:48点击「玉石」→「和田玉」→「籽料」→「红皮」地址栏显示?cid103pid102gid101证明category.parent_id递归查询有效「参数和属性到底啥区别」03:30–04:05同一商品页左侧「参数详情」区显示「润度油润」「质地胶质」右侧「商品描述」区显示「主石南红玛瑙」「适合人群收藏爱好者」文字标注「参数技术指标属性营销话术」「规格组合怎么避免库存超卖」05:12–05:40后台「库存管理」页手动将「10mm红皮籽料」库存设为 1前台立即购买成功再次刷新商品页显示「库存不足」证明product_spec.stock字段实时校验「密码真的不可逆吗」01:55–02:10后台「用户管理」页导出 Excelpassword列全为U2FsdGVkX1...开头的 Base64 字符串无明文痕迹「数据库设计有没有考虑扩展性」00:45–01:20ER 图特写db_design.pngparam_group表含sort_order字段param_item表含is_required字段product_param表含created_time字段全部预留扩展位提示答辩前务必把视频看到第三遍对着镜头暂停自问自答——这比背稿管用十倍。5.2 PPT 的技术页排版心机用对比表格代替文字堆砌wenwan_ppt.pptx的第 12 页「数据库设计对比」不是罗列字段而是用三栏表格直击痛点设计维度传统毕设常见做法本系统做法优势分类存储category表仅两级id,name,parent_idparent_id0表示一级category表含level字段1/2/3path字段存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.DriverTomcatlib目录缺失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 classfilter-section h3文玩特色筛选/h3 label玉质润度select namemoisture option value全部/option option value油润油润/option option value糯性足糯性足/option option value胶质感强胶质感强/option /select/label label皮色select nameskinColor option value全部/option option value洒金洒金/option option value枣红枣红/option option value秋梨秋梨/option /select/label button typesubmit筛选/button /div然后修改ProductListServlet.java的doGet()方法在构建 SQL 时追加// 原 SQLString 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)在答辩时很危险——万一忘记删控制台刷屏。换成 Log4j2pom.xml添加依赖若用 Mavendependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.17.1/version /dependencysrc/log4j2.xml配置?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders File nameFileOut fileNamelogs/wenwan.log PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refFileOut/ /Root /Loggers /ConfigurationUserDAO.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 Test3 秒跑过绿条一闪——这就是实锤。从那以后我每次写开题报告都强制走一遍单元测试哪怕只是assertEquals两个字符串。因为答辩不是考你会不会写是考你敢不敢让代码在所有人面前跑起来。希望帮到你。本文还有配套的精品资源点击获取