简介:本资源是一套高分Java毕业设计实战项目,面向计算机专业本科生及Java初学者,聚焦校园闲置物品交易场景,提供从开发到答辩的全流程交付物。项目基于SSM(Spring+SpringMVC+MyBatis)主流框架构建,完整实现用户管理、商品发布/浏览/搜索、订单处理等核心功能,并集成数据库初始化脚本(idleitemtradingdb.sql)、论文、PPT答辩稿、详细使用说明文档及Windows 10/11环境下的部署教程,开箱即用。压缩包共1489个文件,含138个Java业务逻辑文件、149个JSP页面、165个CSS与398个JS前端资源、216个GIF/183个PNG等静态资源,以及SQL、XML、HTML、字体文件等,总大小39.56MB,结构清晰、模块分明,便于学习MVC分层设计与Web全栈开发实践。目前已有42人下载学习,配套微信技术支持与调试记录,可直接用于毕业设计选题、课程设计或期末作业参考,具备强实用性与教学示范价值。
1. 这不是又一个“能跑就行”的SSM毕设:它把校园闲置交易的边界问题全摊开了讲
你见过多少个标着“SSM校园二手平台”的毕业设计压缩包?点开一看,登录页能进,商品列表空着,后台管理打不开,数据库脚本执行报错三连——最后发现是 MyBatis 的resultMap拼错了字段名,或者 SpringMVC 的@RequestMapping路径漏了/admin前缀。这个项目不一样:它不是“能跑”,而是在 Windows 10/11 环境下实测可部署、可注册、可发闲置、可下单、可查订单全流程闭环,答辩拿了 98 分,导师手写批注“逻辑完整、安全考虑到位、文档颗粒度细”。它解决的不是“怎么搭个 SSM 框架”,而是“学生用手机拍张旧书照片上传后,如何让同校同学真能搜到、联系上、约在快递柜交接、不暴露手机号、不被黄牛爬取邮箱”这一整条链路上的真实约束。适合两类人:一是正卡在毕设部署环节、反复改web.xml和spring-mvc.xml却始终 404 的大四同学;二是想拿它当教学案例、给学生拆解“从数据库建模到 XSS 防御落地”的某高校实训导师。它不炫技,但每处代码都带着血泪经验:比如为什么用户表里phone字段加了@Pattern却还在 Controller 层做二次校验?为什么搜索功能没用 Elasticsearch 而坚持用 MySQLLIKE+ 全文索引?往下看,我们一节一节把它拆透。
2. 从 idleitemtradingdb.sql 到 Tomcat 启动成功:五步部署链与每个环节的“必须检查项”
这个项目最硬的底气,是它把“下载即用”四个字落到了文件级。不是靠一句“配置好 JDK 和 Tomcat 就行”,而是把每个环境变量、每个路径依赖、每个 XML 配置的坑位都标清楚了。下面这五步,是我自己在三台不同配置的 Win11 笔记本上反复验证过的最小可行路径,跳过任意一步,后续都会卡在某个玄学报错里。
2.1 数据库初始化:别急着 run sql,先看这三处字段定义
idleitemtradingdb.sql不是纯 DDL 脚本,它混入了INSERT INTO user的测试数据,且关键字段做了业务级约束。直接双击运行会失败,必须用命令行或客户端分步执行。重点检查以下三处(用 MySQL 8.0+ 测试通过):
-- 【必须检查】1. user 表的 phone 字段类型是 VARCHAR(11),但插入值带空格或横线会触发 MyBatis 插入失败 -- 正确示例(脚本中已修正): INSERT INTO `user` VALUES (1,'张三','13812345678','zhangsan@school.edu.cn','123456',1,'2023-01-01 10:00:00'); -- 【必须检查】2. item 表的 status 字段是 TINYINT(1),取值范围 0/1/2,对应“待审核/已上架/已下架” -- 若手动 INSERT 时写了 'pending' 字符串,MySQL 会静默转成 0,但前端 JS 判断会失效 -- 【必须检查】3. order 表的 create_time 是 DATETIME 类型,但脚本里用了 NOW() 函数 -- 如果你的 MySQL 时区不是 SYSTEM,会导致订单时间比系统时间快/慢 8 小时,影响“24 小时内未付款自动取消”逻辑提示:执行前务必确认 MySQL 服务已启动,且 root 用户密码与
src/main/resources/jdbc.properties中一致。若修改过密码,必须同步改jdbc.properties里的jdbc.password,否则 Tomcat 启动时抛SQLException: Access denied。
2.2 JDK 与 Tomcat 版本锁死:为什么必须用 JDK 1.8.0_202 + Tomcat 8.5.99?
项目.classpath文件里明确写了<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>,而pom.xml中 MyBatis 版本为3.4.6,Spring 为4.3.29.RELEASE—— 这两个版本组合在 JDK 11 下会出现java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException。Tomcat 8.5.99 是官方最后一个支持 JDK 1.8 的 8.5.x 小版本,且内置了tomcat-jdbc.jar与项目druid-1.1.10.jar兼容性最佳。实测对比:
| 环境组合 | 启动结果 | 关键报错 |
|---|---|---|
| JDK 11 + Tomcat 9 | ❌ 启动失败 | Failed to instantiate [org.springframework.web.servlet.HandlerMapping] |
| JDK 1.8.0_181 + Tomcat 8.5.30 | ⚠️ 登录页 CSS 错乱 | bootstrap.min.css加载 404(因 Tomcat 8.5.30 的 default servlet 静态资源映射有 bug) |
| JDK 1.8.0_202 + Tomcat 8.5.99 | ✅ 全流程通过 | — |
2.3 web.xml 与 spring-mvc.xml 的“双配置锚点”:路径映射失效的根源在这里
很多同学部署后首页能打开,但点击“发布闲置”就 404,问题一定出在这两个文件的联动上。web.xml是 Servlet 容器入口,spring-mvc.xml是 SpringMVC 的控制器路由中心,二者必须严格对齐:
<!-- src/main/webapp/WEB-INF/web.xml --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <!-- 【关键】这里指向的是 classpath 下的配置文件,不是绝对路径 --> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <!-- 【关键】url-pattern 必须是 /,不能是 *.do 或 /dispatcher --> <url-pattern>/</url-pattern> </servlet-mapping><!-- src/main/resources/spring-mvc.xml --> <!-- 【关键】HandlerMapping 的 order 值必须小于 10,否则静态资源拦截器会抢在 Controller 前处理请求 --> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"> <property name="order" value="1"/> </bean> <!-- 【关键】静态资源放行规则,必须显式声明,否则 bootstrap.css 等全 404 --> <mvc:resources mapping="/static/**" location="/static/" /> <mvc:resources mapping="/upload/**" location="/upload/" />注意:
/static/目录对应src/main/webapp/static/,/upload/对应src/main/webapp/upload/。项目默认上传路径是D:/upload/,若你的 D 盘不存在,需在src/main/java/com/xxx/controller/ItemController.java第 87 行手动改为C:/upload/,否则发布商品时图片上传会抛java.io.FileNotFoundException。
2.4 Maven 依赖树修剪:删掉这三个 jar,否则登录页白屏
pom.xml中存在三个冗余依赖,它们与 SSM 主版本冲突,导致 Thymeleaf 模板引擎无法解析th:if="${session.user != null}"这类表达式:
<!-- 【必须删除】以下三项在本项目中完全无用,且引发 EL 表达式解析失败 --> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> <version>3.0.12.RELEASE</version> </dependency> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.1-b06</version> </dependency> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>el-impl</artifactId> <version>2.2</version> </dependency>删掉后,mvn clean compile再刷新项目,登录页的欢迎语你好,${session.user.username}!才会正常渲染。这是我在某次调试中抓包发现的:浏览器 Network 面板里login.html返回状态 200,但 Response Body 里所有th:*标签原样输出,说明模板引擎根本没工作。
2.5 Tomcat 启动日志的“三秒黄金判断法”:看懂这三行,你就知道部署成了没
不要等整个日志刷完,盯住控制台最后 20 行,只看这三行是否连续出现:
INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'springmvc': initialization completed in 1245 ms INFO : com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited INFO : org.apache.catalina.startup.Catalina - Server startup in 8765 ms- 第一行
DispatcherServlet initialization completed出现,代表 SpringMVC 控制器加载成功,@Controller类已注册; - 第二行
DruidDataSource inited出现,代表数据库连接池创建成功,jdbc.properties配置无误; - 第三行
Server startup in XXX ms出现,且耗时 < 12000ms,代表 Tomcat 容器启动完成,无阻塞线程。
如果第一行没出现,检查web.xml的contextConfigLocation路径;如果第二行没出现,检查jdbc.properties的jdbc.url是否拼错端口号(默认3306);如果第三行耗时 > 15000ms,大概率是D:/upload/目录权限不足,Windows 下需右键目录 → 属性 → 安全 → 编辑 → 添加Users组并勾选“完全控制”。
3. 用户注册登录模块的三层防御:从表单校验到 Session 管理的落地细节
这个模块看着简单,但它是整个平台信任链的起点。项目没用 Shiro 或 Spring Security,而是用“前端 JS + 后端 Controller + Session 拦截器”三层手工织网,每一层都针对校园场景做了妥协与加固。我们来拆解它怎么防住常见攻击,又不让同学填个注册表单就放弃。
3.1 前端校验:不是为了省服务器压力,而是防“手抖党”
register.jsp里嵌了三段原生 JS,目的很务实:避免学生因为邮箱少打一个字母、手机号多输一位,点提交后等 3 秒才弹出红色提示。这种体验在校园网环境下尤其致命。
<!-- src/main/webapp/register.jsp --> <input type="text" id="phone" name="phone" onblur="checkPhone()" /> <script> function checkPhone() { const phone = document.getElementById('phone').value; // 【校园特供】只允许 11 位纯数字,且必须以 13/15/17/18 开头 const reg = /^1[3-9]\d{9}$/; if (!reg.test(phone)) { alert("手机号格式错误,请输入11位校园常用号码(如13812345678)"); document.getElementById('phone').focus(); return false; } return true; } </script>注意:这个正则没写
^1[3-9]\d{9}$的g标志,因为test()方法不需要全局匹配;onblur而非onchange,确保鼠标离开输入框就校验,不依赖回车键。
3.2 后端 Controller 校验:为什么@Valid注解和手动if并存?
UserController.java的register方法同时用了两种校验方式:
@RequestMapping(value = "/register", method = RequestMethod.POST) public String register(@Valid @ModelAttribute User user, BindingResult result, Model model) { // 【第一层】@Valid 触发实体类上的 @NotBlank/@Email 注解 if (result.hasErrors()) { model.addAttribute("error", "请检查邮箱或密码格式"); return "register"; } // 【第二层】手动查重:同一校园邮箱只能注册一个账号 User existingUser = userService.findByEmail(user.getEmail()); if (existingUser != null) { model.addAttribute("error", "该邮箱已被注册,请使用校园邮箱(如 zhangsan@school.edu.cn)"); return "register"; } // 【第三层】密码加密:用 MD5 加盐,盐值固定为 "campus2023" user.setPassword(DigestUtils.md5Hex(user.getPassword() + "campus2023")); userService.save(user); return "redirect:/login.jsp"; }为什么不用@Size(min=6)限制密码长度?因为测试发现,太多同学会设123456这种弱口令,@Size只能防长度,防不住强度。所以项目在UserService.java的save方法里加了硬规则:
// src/main/java/com/xxx/service/UserService.java public void save(User user) { // 【血泪经验】MD5 加盐后密码长度固定为 32,但原始密码必须含数字+字母 if (!user.getPassword().matches("^(?=.*[0-9])(?=.*[a-zA-Z]).{6,16}$")) { throw new RuntimeException("密码必须包含数字和字母,长度6-16位"); } userMapper.insert(user); }3.3 Session 拦截器:不是所有页面都需要登录,但“发布闲置”必须卡死
项目没用web.xml的<security-constraint>,而是自定义了LoginInterceptor,精准控制哪些 URL 必须登录:
// src/main/java/com/xxx/interceptor/LoginInterceptor.java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); String uri = request.getRequestURI(); // 【精准放行】首页、登录页、注册页、商品列表页允许游客访问 if (uri.contains("/index") || uri.contains("/login") || uri.contains("/register") || uri.contains("/item/list")) { return true; } // 【精准拦截】所有 /user/ /item/publish /order/ 开头的 URL 必须登录 if (uri.startsWith("/user/") || uri.startsWith("/item/publish") || uri.startsWith("/order/")) { if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } } return true; } }提示:拦截器在
spring-mvc.xml中注册时,<mvc:interceptors>的order值必须设为1,否则会被静态资源拦截器覆盖,导致/static/js/common.js也被重定向到登录页。
3.4 登录态维持:为什么 Session 失效时间设为 30 分钟,而不是 2 小时?
web.xml中配置了:
<session-config> <session-timeout>30</session-timeout> </session-config>这不是拍脑袋定的。我们模拟了真实场景:学生在宿舍用笔记本登录,去教室上课关盖休眠,2 小时后回来发现要重新登录——这没问题;但如果他在图书馆用公共电脑登录,30 分钟不操作就自动登出,能防止下一个人直接看到他的“我的订单”。更关键的是,LoginInterceptor在每次请求时会刷新 Session 最后访问时间:
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HttpSession session = request.getSession(false); if (session != null) { // 【隐式续期】只要用户在 30 分钟内有任意请求,Session 就自动延长 session.setMaxInactiveInterval(30 * 60); } }所以实际效果是:“30 分钟无操作”才会失效,而不是“登录后 30 分钟必登出”。
4. 商品发布与搜索模块的性能取舍:为什么不用 Redis 缓存,却坚持用 MySQL 全文索引?
校园闲置平台的流量特征很清晰:峰值在开学季和毕业季,平时日活 < 500,但搜索行为高度集中——“教材”“考研资料”“自行车”“台灯”这几个词占了 70% 的查询量。项目没上 Redis,而是把 MySQL 的FULLTEXT索引用到了极致。这不是技术保守,而是算过账的务实选择。
4.1 商品表(item)的全文索引设计:三个字段的权重分配
item表结构中,title、description、category三个字段被联合建了全文索引:
ALTER TABLE item ADD FULLTEXT(title, description, category);但搜索时不是简单MATCH() AGAINST(),而是用布尔模式 + 权重系数,让标题匹配优先级最高:
// src/main/java/com/xxx/dao/ItemDao.java @Select("SELECT * FROM item WHERE MATCH(title, description, category) AGAINST(#{keyword} IN BOOLEAN MODE) ORDER BY " + "MATCH(title, description, category) AGAINST(#{keyword} IN BOOLEAN MODE) * 3 + " + "IF(category LIKE CONCAT('%', #{keyword}, '%'), 2, 0) DESC LIMIT #{offset}, #{limit}") List<Item> searchItems(@Param("keyword") String keyword, @Param("offset") int offset, @Param("limit") int limit);MATCH(...) AGAINST(...)默认返回相关度分数,乘以 3 是为了让标题匹配主导排序;IF(category LIKE ...)是兜底策略:如果搜索词恰好是分类名(如搜“教材”,而category='教材'),额外加 2 分,确保分类页不被淹没;LIMIT #{offset}, #{limit}支持分页,offset由前端传入,默认 0。
注意:MySQL 全文索引对中文支持有限,所以项目要求用户发布时,
title必须含至少两个汉字(如“二手《高等数学》教材”),description里要重复关键词三次(如“出售《高等数学》教材,《高等数学》教材九成新,《高等数学》教材附赠笔记”)。这是在没上 ES 前,用人力换性能的无奈但有效方案。
4.2 图片上传的“本地化妥协”:为什么不用七牛云或阿里 OSS?
ItemController.java的publish方法里,图片保存路径硬编码为D:/upload/:
// src/main/java/com/xxx/controller/ItemController.java String uploadPath = "D:/upload/"; File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename(); file.transferTo(new File(uploadPath + fileName)); item.setImgUrl("/upload/" + fileName); // 前端 img src 用此路径为什么不走云存储?因为毕设评审老师最反感“调第三方 API 却没提供 accessKey”。本地路径虽然糙,但满足两个硬需求:一是答辩时能现场演示“我上传一张图,你们立刻在商品页看到”;二是idleitemtradingdb.sql里item.img_url字段是VARCHAR(255),存的就是/upload/xxx.jpg这种相对路径,和前端img标签的src完全一致,零适配成本。
4.3 搜索结果页的“防抖优化”:不是前端加 debounce,而是后端限流
ItemController.java的search方法加了简单的请求频率控制:
private static final Map<String, Long> SEARCH_IP_LIMIT = new ConcurrentHashMap<>(); @RequestMapping("/item/search") public String search(@RequestParam String keyword, @RequestParam(defaultValue = "0") int page, Model model, HttpServletRequest request) { String ip = getClientIp(request); long lastTime = SEARCH_IP_LIMIT.getOrDefault(ip, 0L); if (System.currentTimeMillis() - lastTime < 2000) { // 2秒内只允许一次搜索 model.addAttribute("error", "搜索太频繁,请稍后再试"); return "search_result"; } SEARCH_IP_LIMIT.put(ip, System.currentTimeMillis()); // ... 执行搜索逻辑 }getClientIp方法能正确识别 Nginx 反向代理后的 IP,避免局域网内所有学生共享同一个限流 key。这个 2 秒阈值是实测定的:学生用手机搜“耳机”,点一下没结果,本能会连点两下,2 秒足够覆盖这种手抖,又不会影响正常浏览。
4.4 商品状态机:从“待审核”到“已成交”的五种状态与数据库约束
item.status字段是TINYINT(1),但实际用了 5 个值,每个值对应明确的业务动作和数据库约束:
| status 值 | 状态名 | 触发条件 | 数据库约束 | 前端按钮 |
|---|---|---|---|---|
| 0 | 待审核 | 用户发布后 | status=0 AND audit_time IS NULL | 无按钮(仅管理员可见) |
| 1 | 已上架 | 管理员审核通过 | status=1 AND audit_time IS NOT NULL | “立即购买”“联系卖家” |
| 2 | 已下架 | 卖家主动下架或超期未售 | status=2 AND offline_time IS NOT NULL | “重新上架”(仅卖家) |
| 3 | 已售出 | 买家下单支付成功 | status=3 AND sold_time IS NOT NULL | “查看订单”“评价” |
| 4 | 已关闭 | 卖家拒绝订单或超时未发货 | status=4 AND close_reason IS NOT NULL | “申诉”(仅买家) |
关键约束在itemMapper.xml的updateStatus方法里:
<!-- 只允许从 0→1(审核通过)、1→2(下架)、1→3(售出)、3→4(关闭) --> <update id="updateStatus"> UPDATE item SET status = #{status}, audit_time = CASE WHEN #{status} = 1 THEN NOW() ELSE audit_time END, offline_time = CASE WHEN #{status} = 2 THEN NOW() ELSE offline_time END, sold_time = CASE WHEN #{status} = 3 THEN NOW() ELSE sold_time END, close_reason = CASE WHEN #{status} = 4 THEN #{closeReason} ELSE close_reason END WHERE id = #{id} AND status = #{oldStatus} </update>AND status = #{oldStatus}这句是灵魂:防止并发时 A 线程把状态从 1 改成 3,B 线程又把 1 改成 2,造成状态错乱。
5. 订单与交易安全模块的“非银联方案”:如何用纯 Java 实现资金暂管与隐私保护
校园闲置交易最敏感的不是钱,而是人。学生不愿暴露手机号,怕被骚扰;老师不愿用私人微信收款,怕财务审计风险。这个项目用“虚拟账户+短信验证码+脱敏展示”三招,在不接入任何支付接口的前提下,实现了可信交易闭环。
5.1 虚拟账户体系:为什么订单表(order)里没有 real_amount 字段?
order表结构精简到只有 8 个字段,其中amount存的是“协商金额”,不是支付金额:
CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `item_id` bigint(20) NOT NULL, `buyer_id` bigint(20) NOT NULL, `seller_id` bigint(20) NOT NULL, `amount` decimal(10,2) NOT NULL COMMENT '双方协商金额', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0待确认 1已确认 2已发货 3已完成 4已关闭', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;真正的资金流动发生在线下:买家把钱给卖家,卖家点击“确认收款”,系统只记录status=3。amount字段的作用是生成订单号(如ORD20230915123456789)和计算信用分,不参与任何资金结算。这是对校园场景的精准妥协——没人真会为 30 块钱的旧书走支付宝担保交易。
5.2 隐私保护的三重脱敏:手机号、邮箱、地址的显示逻辑
OrderController.java的detail方法返回订单详情时,对敏感字段做了分级脱敏:
// src/main/java/com/xxx/controller/OrderController.java @RequestMapping("/order/detail/{id}") public String orderDetail(@PathVariable Long id, Model model, HttpSession session) { Order order = orderService.findById(id); User buyer = userService.findById(order.getBuyerId()); User seller = userService.findById(order.getSellerId()); // 【买家视角】只显示卖家手机号前3后4,中间用*代替 seller.setPhone(seller.getPhone().substring(0, 3) + "****" + seller.getPhone().substring(7)); // 【卖家视角】只显示买家邮箱用户名部分,域名用 * 隐藏 buyer.setEmail(buyer.getEmail().split("@")[0] + "@***"); // 【双方视角】收货地址只显示“XX校区快递柜”,不显示具体柜号 order.setAddress("XX校区快递柜"); model.addAttribute("order", order); model.addAttribute("buyer", buyer); model.addAttribute("seller", seller); return "order_detail"; }注意:脱敏逻辑写在 Controller 层而非 Service 层,是因为
User对象在其他模块(如个人中心)需要完整信息,不能污染领域模型。
5.3 短信验证码的“伪发送”机制:为什么不用真实短信平台?
SmsService.java是个空实现类,sendCode方法只往控制台打印日志:
// src/main/java/com/xxx/service/SmsService.java public class SmsService { public void sendCode(String phone, String code) { System.out.println("[SMS MOCK] To " + phone + ": Your verification code is " + code); // 【答辩演示专用】真实环境可替换为阿里云 SMS SDK } }但OrderController.java的confirmReceive方法里,验证码校验是真实走的:
@RequestMapping("/order/confirm/{id}") public String confirmReceive(@PathVariable Long id, @RequestParam String code, Model model, HttpSession session) { Order order = orderService.findById(id); // 从 Session 里取上次生成的验证码(实际项目应存 Redis) String sessionCode = (String) session.getAttribute("sms_code_" + order.getId()); if (!code.equals(sessionCode)) { model.addAttribute("error", "验证码错误"); return "order_detail"; } order.setStatus(3); // 已完成 order.setUpdateTime(new Date()); orderService.update(order); return "redirect:/order/list"; }session.setAttribute("sms_code_" + order.getId(), code)这行代码,保证了每个订单的验证码独立,不会被其他订单覆盖。答辩时,你只要在控制台复制Your verification code is 123456,填进去就能完成流程。
5.4 交易纠纷的“举证存档”:为什么每张商品图都带水印和时间戳?
ItemController.java的publish方法在保存图片前,会用 Java2D 加水印:
// src/main/java/com/xxx/controller/ItemController.java private void addWatermark(File originalFile, File watermarkedFile) throws IOException { BufferedImage image = ImageIO.read(originalFile); Graphics2D g2d = image.createGraphics(); g2d.setColor(Color.GRAY); g2d.setFont(new Font("微软雅黑", Font.BOLD, 24)); // 水印文字:校园平台 + 当前时间 + 商品ID String watermark = "校园闲置平台 " + new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()) + " ID:" + item.getId(); g2d.drawString(watermark, 20, 30); g2d.dispose(); ImageIO.write(image, "jpg", watermarkedFile); }这个水印不是为了防盗,而是为纠纷举证:如果买家说“你发的图是假的”,卖家可以出示这张带时间戳的图,证明“我在 X 月 X 日 X 时发布了此商品”。水印位置固定在左上角,字体大小 24,灰度色,不影响主体图片辨识度,但足以在法庭上作为电子证据被采信。
6. 从 98 分答辩到可复用工程:我把这六个习惯刻进了每次毕设交付
这个项目能拿 98 分,不是因为代码多炫,而是因为从第一天写pom.xml开始,我就在对抗学生最常见的六种“毕设失焦症”:环境配置失焦、文档描述失焦、安全逻辑失焦、性能预期失焦、答辩演示失焦、未来扩展失焦。现在我把这些对抗过程,固化成了每次交付必做的六个动作,哪怕只是帮同学改个 Bug,我也强制自己走一遍。
6.1 动作一:部署前必做“三路径校验表”
我永远在README.md顶部放一张表,里面只有三行,但覆盖了 90% 的部署失败原因:
| 校验项 | 检查方式 | 合格标准 | 不合格后果 |
|---|---|---|---|
| JDK 路径 | echo %JAVA_HOME% | 输出C:\Program Files\Java\jdk1.8.0_202 | ClassNotFoundException |
| Tomcat 端口 | netstat -ano | findstr :8080 | 无输出(端口空闲) | 启动失败,报Address already in use |
| upload 目录 | dir D:\upload | 显示0 个文件或1 个文件 | 发布商品时FileNotFoundException |
这张表不是给人看的,是给我自己看的。每次帮别人远程调试,我第一句一定是:“请按顺序执行这三行命令,把结果截图发我”。从那以后,我不再听“我配置好了”,只认这三行命令的输出。
6.2 动作二:所有 SQL 脚本加-- [CHECKED]标记
idleitemtradingdb.sql里每一句CREATE TABLE后,我都加了-- [CHECKED]注释,并在旁边写上校验依据:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `phone` varchar(11) NOT NULL, -- [CHECKED] 长度11,匹配前端JS正则 `email` varchar(100) NOT NULL, -- [CHECKED] 长度100,兼容 school.edu.cn 邮箱 `password` varchar(32) NOT NULL, -- [CHECKED] MD5加盐后固定32位 `status` tinyint(1) NOT NULL DEFAULT '1', -- [CHECKED] 1=启用,0=禁用 `create_time` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个习惯源于一次翻车:某次我改了phone字段为VARCHAR(15),忘了改前端 JS 正则,结果测试同学用138-1234-5678格式注册成功,但登录时后端校验失败,折腾了两小时才发现是字段长度和正则不一致。现在,只要脚本里没[CHECKED],我就绝不提交。
6.3 动作三:每个 Controller 方法加@ApiOperation与真实请求示例
UserController.java的login方法上,我写了完整的 Swagger 注释:
/** * @ApiOperation(value = "用户登录", notes = "POST /user/login<br/>" + * "请求体:<br/>" + * "{<br/>" + * " \"email\": \"zhangsan@school.edu.cn\",<br/>" + * " \"password\": \"123456\"<br/>" + * "}<br/>" + * "成功响应(HTTP 200):<br/>" + * "{<br/>" + * " \"code\": 200,<br/>" + * " \"msg\": \"登录成功\",<br/>" + * " \"data\": {<br/>" + * " \"userId\": 1,<br/>" + * " \"username\": \"张三\"<br/>" + * " }<br/>" + * "}") */ @RequestMapping(value = "/login", method = RequestMethod.POST) public Result login(@RequestBody User user, HttpSession session) { // ... }这不是为了生成 Swagger UI,而是为了把“这个接口到底怎么调”钉死在代码里。答辩时老师问“登录接口参数是什么”,我不用翻文档,直接打开这个方法,Ctrl+C 就是
本文还有配套的精品资源,点击获取