ARTICLE DETAIL

资讯详情

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

JSP+SSM校园闲置物品交易平台毕设全解析:从数据库到核心代码

JSP+SSM校园闲置物品交易平台毕设全解析:从数据库到核心代码 每年到这个节点总有一批人被毕业设计折腾得焦头烂额。如果你打开这篇内容大概率也是拿到或者正准备选一个类似的课题基于JSP和SSM的校园闲置物品交易平台。先给你吃颗定心丸——这个题一点都不“土”它反而是我推荐技术基础一般、又想稳过答辩的同学优先考虑的方向。JSP负责页面展示SSMSpring SpringMVC MyBatis负责业务逻辑和数据操作整套组合解决的问题非常直接学生之间怎么发布闲置、搜索商品、下单交易、管理个人信息同时给管理员留出用户和商品管理的后台入口。这篇文章不是给你贴一大段复制粘贴的代码而是按照我当年做这个题的真实推进顺序来写从“为什么选这个技术栈”到“数据库表怎么设计”从“SSM配置文件怎么组织”到“发布商品、下单交易的核心代码链路”最后再把我踩过的几个代表性坑——比如JSP改了不生效、图片视频路径错乱、两个人同时下单怎么保证数据一致性——一次性讲清楚。你按这个思路做完不只是“能运行”而是答辩时每个设计决策都能说出来龙去脉。1. 为什么是“JSP SSM”选题价值与功能版图1.1 老技术栈为什么反而是毕业设计的稳妥之选我见过不少同学一上来就奔着前后端分离去Spring Boot Vue Element UI看起来很潮结果光跨域、Token、路由守卫就折腾了两个星期到最后论文里自己写的东西没多少全是配置。毕业设计的评分逻辑和真实互联网项目的逻辑不一样评审老师更在意的是业务闭环是否完整、技术方案是否合理、你对整个系统是否真的理解。JSP SSM这套组合恰恰是“理解成本”最低的方案。JSP是服务端渲染页面Java代码片段和JSTL标签可以直接嵌在HTML里数据怎么来、怎么展示一眼就能看明白不需要额外学前端工程化那套东西。SSM三个成员的分工也很清晰Spring管对象、SpringMVC管请求分发、MyBatis管数据库CRUD正好对应一个Web项目从“用户浏览器发出请求”到“数据库返回结果”的完整链路。而且这套技术栈和Spring Boot并没有断层。你搞清楚了Spring容器的IOC、AOP这些核心思想以后写SpringBoot项目只是把XML配置换成了自动装配注解底层逻辑完全一致。转型成本很低但作为本科毕业设计的考察点它反而比“一键生成”的脚手架更能体现个人工作量。1.2 交易场景的角色、故事线与产品取舍先别急着打开IDE写代码花半小时把“这个系统到底服务谁”想明白。校园闲置物品交易平台核心场景就一句话某位同学手里有闲置的教材、自行车、数码产品挂到平台上另一个同学看到后联系购买双方线下完成交接。围绕这个场景有三类角色游客可以浏览商品列表、搜索、查看商品详情。为什么游客要看详情因为登录是个转化门槛如果浏览都要登录体验差演示时也会显得系统很封闭。注册用户可以发布闲置物品、编辑自己发布的商品、下架商品可以购买别人的商品可以收藏感兴趣的商品还拥有个人中心管理自己的资料和订单。管理员维护分类、管理用户、管理商品状态还可以把订单数据导出成Excel做简单统计。这个模糊的业务描述里藏着一个最重要的产品决策校园闲置交易不需要做线上支付。因为交易双方大概率是同一所学校甚至同一栋宿舍楼的人真实场景就是线下面交。你把支付系统接进来反而引入了一堆安全和合规问题对毕设来说属于明显的过度设计。所以我当时把订单状态设计成了“待确认 → 已完成 / 已取消”这种足够简单但逻辑完整的状态机这样既能形成交易闭环又不会把战线拉长。1.3 功能清单与权限矩阵下面这组功能清单是我最终落地到项目里的版本你可以直接把它抄到开题报告或者论文的“功能需求分析”里。这里我用权限矩阵来展示每一行代表一个功能入口列代表能使用它的角色功能模块游客注册用户管理员说明商品浏览与搜索✅✅✅按分类筛选、按关键词模糊搜索商品详情查看✅✅✅展示图片、描述、卖家信息、浏览量注册与登录✅✅✅注册后自动写入用户表登录状态用Session维护发布闲置商品❌✅✅需要登录管理员也可以代发布编辑/下架/删除商品❌✅✅只能操作自己发布的商品下单购买❌✅✅一物一单不能买自己发布的商品确认完成交易❌✅❌买家确认后订单变为已完成取消订单❌✅❌取消后商品自动恢复为“在售”收藏商品❌✅❌收藏记录可查可删个人中心❌✅✅展示个人信息、头像、统计数量后台用户管理❌❌✅启用/禁用账号后台商品管理❌❌✅下架违规商品订单数据导出Excel❌❌✅把订单列表写入xls文件分类管理❌❌✅增加、修改分类名称这个表格直接展示了系统的全貌同时也确定了数据库里必须有用户表、商品表、订单表、分类表、收藏表这五张基础表至于要不要留言表就看你是否打算做“商品咨询”这个功能下一章会细讲。1.4 工程目录:约定优于配置的落地我当时建立工程目录时吃过一个亏——包名太乱后面自己都找不到东西。推荐你按下面的方式组织既符合Java Web惯例也方便后续写论文画架构图src/main/java/com/campus/ ├── common/ # 通用类Result返回体、常量、分页结果 ├── controller/ # SpringMVC控制层 ├── service/ # 业务接口 │ └── impl/ # 业务实现类 ├── mapper/ # MyBatis数据访问接口 ├── entity/ # 数据库实体类 ├── vo/ # 视图模型联表查询的结果对象 └── util/ # 工具类文件上传、Excel导出、加密JSP页面我建议统一放在src/main/webapp/WEB-INF/views/下然后通过SpringMVC的视图解析器跳转。这样做有一个安全上的好处WEB-INF目录下的资源无法被浏览器直接通过URL访问用户必须经过Controller转发才能看到页面可以挡住一些直接请求.jsp文件的投机行为。2. 数据库是地基六张核心表的设计思路数据库设计决定了你后面写代码是顺风顺水还是反复返工。很多同学拿到题目就急着建表结果做到订单模块发现少了一个字段回头再改表连带实体类、Mapper、页面全要动一遍非常折磨。我按当时的建表顺序把每一张表的设计意图讲给你。2.1 用户表除了账号密码还要存什么用户表是整个系统的核心因为商品、订单、收藏都通过外键关联到用户。除了常规的id、username、password之外我建议加上这些字段字段名类型说明nicknamevarchar(50)昵称首页和商品详情页显示用avatarvarchar(255)头像图片路径不存二进制只存路径phonevarchar(20)联系方式线下交易需要互换手机号wechatvarchar(50)微信号比手机号更常用campusvarchar(50)所在校区或宿舍区用于筛选同区域交易roletinyint0表示普通用户1表示管理员statustinyint0正常1禁用被管理员封号后无法登录create_timedatetime注册时间密码不要明文存储至少用MD5做一次散列。网上有现成的MD5工具类几行代码的事。答辩时老师问“为什么密码不能明文存”你要能回答出“数据库泄露后用户在其他平台可能用相同密码也容易被脱库撞库”这类理由。2.2 商品表状态字段决定了交易流程的边界商品表是信息展示的主体。除了常规的标题、描述、价格我特别强调两个设计点第一价格字段用decimal(10,2)不用float更不用double。二进制浮点数在表示小数时存在精度误差0.1 0.2 可能等于 0.30000000000000004这在涉及钱的地方是不能接受的。Decimal是字符串存储的定点数专门为金额这类场景设计。第二状态字段 status 是整个交易流程的开关。我采用的是下面这一套约定0 在售1 已售出2 已下架3 待审核如果你做了后台审核功能每一个状态都直接影响用户在前台能做什么操作。比如商品状态为1时“立即购买”按钮必须禁用状态为2时只有卖家自己能看到“重新上架”入口。这台状态机看起来简单但它在“发布商品 → 买家下单 → 卖家下架 → 交易完成”这条链路上起了核心作用。商品表还需要 cover_image封面图和 images多图多个路径可以用逗号分隔存在一个字段里也可以另建一张商品图片表。对于毕设来说逗号分隔存一个字段是性价比最高的方案减少一张表查询时按逗号split一下就能展示。2.3 订单表一物一单与价格快照思想订单表是整个平台最关键的“事实表”。校园闲置交易有一个特点每一件商品都是孤品不存在库存数量大于1的情况所以商品和订单是严格的一对一关系。这个认知很重要它直接决定了订单表不需要订单明细、不需要购物车因为买家不会同时对同一件商品买两件。订单表核心字段如下字段名类型说明order_novarchar(32)唯一订单号展示给用户看的product_idint关联商品表buyer_idint买家IDseller_idint卖家IDpricedecimal(10,2)下单时的成交价格快照statustinyint0待确认、1已完成、2已取消remarkvarchar(255)订单备注create_timedatetime下单时间finish_timedatetime交易完成时间这里有一个非常值得在论文和答辩中讲清楚的概念price为什么要在订单表冗余一份而不直接去查商品表因为卖家在商品被下单后依然可能修改商品价格如果订单详情页每次去读商品表买家看到的金额可能和下单时不一致。订单表保存一份“成交价格快照”意思就是这笔交易一旦生成价格就固定了后续任何商品表改动都不影响历史订单。这是真实交易系统里非常常见的设计思路。2.4 分类、收藏与留言轻量化扩展设计分类表最简单只有id、name、sort_order三个字段用于前台分类筛选和后台分类管理。收藏表两个业务字段user_id 和 product_id再加一个 create_time并且一定要给(user_id, product_id)建联合唯一索引防止用户对同一个商品收藏两次。留言表是可选的。我当时做的时候没有独立做留言功能而是把咨询逻辑简化成了“商品详情页展示卖家微信/手机号线下沟通”。如果你想多一个功能亮点可以做一张留言表product_id、from_user_id、content、create_time显示在商品详情页底部实现类似“问一问”的效果。这个功能成本低但对提升系统的交互感很有帮助而且答辩时可以让老师看到你考虑了买卖双方的沟通需求。2.5 核心建表SQL与字段类型细节下面是用户表、商品表、订单表三张核心表的建表SQL基本覆盖了前面讲的设计要点。直接复制到Navicat或者命令行执行都行CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT MD5散列后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像路径, phone varchar(20) DEFAULT NULL, wechat varchar(50) DEFAULT NULL, campus varchar(50) DEFAULT NULL COMMENT 校区/宿舍区, role tinyint NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, status tinyint NOT NULL DEFAULT 0 COMMENT 0正常 1禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE product ( id int NOT NULL AUTO_INCREMENT, seller_id int NOT NULL COMMENT 发布者ID, category_id int DEFAULT NULL COMMENT 分类ID, title varchar(100) NOT NULL, description text COMMENT 商品描述, price decimal(10,2) NOT NULL COMMENT 出售价格, original_price decimal(10,2) DEFAULT NULL COMMENT 原价/参考价, cover_image varchar(255) DEFAULT NULL COMMENT 封面图路径, images text COMMENT 多图路径逗号分隔, status tinyint NOT NULL DEFAULT 0 COMMENT 0在售 1已售 2下架 3待审核, view_count int NOT NULL DEFAULT 0 COMMENT 浏览量, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT闲置商品表; CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, product_id int NOT NULL, buyer_id int NOT NULL, seller_id int NOT NULL, price decimal(10,2) NOT NULL COMMENT 成交价格快照, status tinyint NOT NULL DEFAULT 0 COMMENT 0待确认 1已完成 2已取消, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, finish_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), UNIQUE KEY uk_product_id (product_id), KEY idx_buyer (buyer_id), KEY idx_seller (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这里有几个细节值得注意所有表都用utf8mb4而不是utf8因为utf8mb4才能完整支持Emoji和生僻字防止用户头像昵称里带个特殊字符导致插入失败。订单表给product_id加了唯一索引它的作用我在后面的并发章节会重点讲。InnoDB引擎支持事务和外键约束语义是SSM项目里最合适的选择。3. SSM整合与核心业务代码的落地3.1 依赖清单与三份核心配置文件的协作关系SSM整合的第一步是搞定Maven依赖。我在pom.xml里维护的依赖清单如下已经去掉版本号你可以按自己用的Spring版本补充dependencies !-- Spring核心 -- dependencyorg.springframework:spring-webmvc/dependency dependencyorg.springframework:spring-jdbc/dependency !-- MyBatis与整合包 -- dependencyorg.mybatis:mybatis/dependency dependencyorg.mybatis:mybatis-spring/dependency !-- 数据库驱动与连接池 -- dependencymysql:mysql-connector-java/dependency dependencycom.alibaba:druid/dependency !-- JSP与JSTL -- dependencyjavax.servlet:javax.servlet-api/dependency dependencyjavax.servlet:jstl/dependency !-- 文件上传 -- dependencycommons-fileupload:commons-fileupload/dependency !-- JSON处理 -- dependencycom.fasterxml.jackson.core:jackson-databind/dependency !-- 分页插件 -- dependencycom.github.pagehelper:pagehelper/dependency !-- Excel导出 -- dependencyorg.apache.poi:poi/dependency /dependencies配置文件的组织方式我建议拆成三份各司其职applicationContext.xmlSpring父容器只扫描service、mapper、entity等非Controller的Bean配置数据源、事务管理器。springmvc.xmlSpringMVC子容器只扫描controller包配置视图解析器、文件上传解析器、静态资源放行。web.xml负责把上面两份配置加载到Tomcat容器里。为什么要分成父子容器而不是一个Spring配置通吃所有这是SSM整合最经典的一个坑点如果两个容器都扫描了同一个Controller会导致请求路径被两个Bean同时处理经常出现“控制器映射冲突”或者事务失效的诡异bug。标准做法是父容器管Service和Mapper子容器只管Controller边界清晰。web.xml里还有两个容易出错的地方。第一个是编码过滤器一定要配置CharacterEncodingFilter并且把forceEncoding设为true否则POST提交的中文很容易乱码。第二个是DispatcherServlet的url-pattern用/会接管所有请求包括静态资源所以必须在springmvc.xml里用mvc:resources放行/static/**和/upload/**。SpringMVC的视图解析器这样配置bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean视图解析器的含义是Controller返回product/detail字符串时SpringMVC会自动拼接出/WEB-INF/views/product/detail.jsp这个物理路径。坚持返回逻辑视图名、不返回物理路径是MVC设计模式的核心要求也是答辩时能体现你理解分层架构的细节。3.2 发布闲置从表单到数据库的完整链路发布闲置是整个平台第一个需要跨多个层次协作的功能。页面是一个表单包含标题、分类、价格、原价、描述、封面图。表单提交时图片已经通过CommonsMultipartResolver上传到了服务器所以Controller接收到的除了表单字段还有MultipartFile。先看Controller层Controller RequestMapping(/product) public class ProductController { Autowired private ProductService productService; PostMapping(/add) ResponseBody public Result add(RequestParam(file) MultipartFile file, Product product, HttpSession session) { User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { return Result.error(请先登录); } // 保存图片返回可访问的URL String imageUrl FileUploadUtil.save(file); product.setCoverImage(imageUrl); product.setSellerId(loginUser.getId()); product.setStatus(0); // 默认在售 productService.addProduct(product); return Result.success(发布成功); } }这里用了统一返回体Result它包装了code状态码、message提示信息、data数据三个字段。为什么要做这个包装因为页面上很多操作是AJAX异步提交的服务端返回统一结构的JSON前端才能统一判断成功失败而不是每个接口各回各的格式。这是一个从第一个接口开始就要养成的习惯。FileUploadUtil.save()方法里有两个实操细节图片保存路径不能写到IDE的target或out目录里因为每次重新编译项目这些目录会被清空导致之前上传的图片全部丢失。我当时是把文件写到项目下的/upload/目录并用UUID生成新文件名防止用户上传的图片名重复或包含中文导致路径乱码public static String save(MultipartFile file) { String realPath System.getProperty(user.dir) /upload/; File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } String originalName file.getOriginalFilename(); String ext originalName.substring(originalName.lastIndexOf(.)); String newName UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(realPath newName)); return /upload/ newName; }Service层要注意这个场景是单表插入事务显得不是很有必要但我们要培养一个习惯所有写操作Service方法都加Transactional注解。这样做的好处是以后某个业务方法里同时操作三张表时你的事务意识已经在而不会因为漏掉注解导致数据只写了一半。商品初始状态直接设为0在售这个状态值最好抽成一个常量类ProductStatusEnum不要散落在代码里。3.3 下单链路校验、乐观锁与事务下单是整个系统最核心、也最值得在答辩时展开讲的业务逻辑。用户点击“立即购买”后后端要做的事情比表面上看起来多得多。我当时把下单方法写成下面这样Override Transactional(rollbackFor Exception.class) public Result createOrder(Integer productId, Integer buyerId) { // 1. 查询商品 Product product productMapper.selectByPrimaryKey(productId); if (product null) { return Result.error(商品不存在或已被删除); } // 2. 状态校验 if (product.getStatus() ! 0) { return Result.error(商品已售出或已下架); } // 3. 不能买自己的商品 if (product.getSellerId().equals(buyerId)) { return Result.error(不能购买自己发布的商品); } // 4. 乐观锁更新status0 - 1 int updated productMapper.updateStatusConditionally(productId, 0, 1); if (updated ! 1) { return Result.error(手慢了商品刚刚被别人买走); } // 5. 生成订单并落库 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setProductId(productId); order.setBuyerId(buyerId); order.setSellerId(product.getSellerId()); order.setPrice(product.getPrice()); order.setStatus(0); orderMapper.insertSelective(order); return Result.success(order); }这个方法里第4步是关键中的关键。updateStatusConditionally对应的SQL长这样update idupdateStatusConditionally UPDATE product SET status #{newStatus}, update_time NOW() WHERE id #{productId} AND status #{oldStatus} /update注意WHERE条件里的status #{oldStatus}它保证了“只有商品当前处于在售状态时才允许被改成已售出”。同时被两个人下单时数据库的行锁会让第二次UPDATE的影响行数为0于是第二个请求就会走进“手慢了”的分支不会生成两张订单。为什么不先查一下状态、再更新、再插入因为查和更新之间有时间差在这个间隙如果有另一个请求抢先更新了商品状态你的校验就失效了。把状态判断放进UPDATE的WHERE条件里是把“检查和修改”合并成了一个原子操作这是并发控制里典型的乐观锁思路。围绕这几十行代码你可以回答一连串答辩问题什么是乐观锁、为什么要用事务、怎么保证不会超卖。订单号的生成我用的是时间戳 三位随机数private String generateOrderNo() { return System.currentTimeMillis() String.format(%03d, new Random().nextInt(1000)); }对校园场景来说已经足够因为还配合了订单表里order_no的唯一索引万一撞了插入时数据库会直接报错你可以在捕获异常后重新生成一个再插入。3.4 订单状态流转与个人中心的数据视角订单创建之后状态流转是这样一个闭环买家下单订单状态 0待确认商品状态 1已售出双方线下完成交易买家在自己的购买列表点击“确认完成”订单状态 1已完成如果双方协商取消买家或卖家取消订单订单状态 2已取消同时商品状态要恢复为 0在售这里有一个容易被忽视的坑取消订单恢复商品状态时也要使用条件更新。也就是说不能直接UPDATE product SET status 0 WHERE id ?而要先确认商品当前状态确实为1已售出。因为有可能出现这样的场景买家A下单后商品变成了已售出但是很长时间没有确认完成卖家误以为交易黄了手动把商品下架重新上架了。此时如果买家取消订单后台再无条件把商品改成在售就和卖家的本意冲突了。条件更新能够避免这种状态混乱。个人中心的数据视角其实就是围绕当前登录用户的ID做几张关联查询不需要新增加页面就能完成“我发布的”列表查询 product 表 where seller_id 当前用户ID“我卖出的”订单查询 orders 表 where seller_id 当前用户ID“我买到的”订单查询 orders 表 where buyer_id 当前用户ID“我的收藏”列表查询 favorite 表 join product 表条件是 favorite.user_id 当前用户ID3.5 个人信息展示与Excel导出个人中心首页的信息展示页面在热搜词里对应“jsp个人信息展示页面”。这个页面看起来简单但要注意两个数据正确性问题。第一个是头像和昵称的刷新问题用户修改资料后session里保存的loginUser还是旧数据如果直接从session里取页面刷新后依然显示修改前的信息。正确做法是修改成功后重新查一遍用户表把新对象set回session里。第二个是统计数字的SQL写法比如“我的在售商品数”一行SQL就能完成select idcountBySellerIdAndStatus resultTypeint SELECT COUNT(*) FROM product WHERE seller_id #{sellerId} AND status #{status} /selectExcel导出是热搜词里也出现过的功能它是毕设一个很好的加分点。用Apache POI实现核心代码如下public void exportOrders(ListOrderVO orderList, HttpServletResponse response) throws Exception { HSSFWorkbook workbook new HSSFWorkbook(); HSSFSheet sheet workbook.createSheet(订单列表); String[] headers {订单号, 商品名称, 买家, 卖家, 成交价格, 状态, 下单时间}; HSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum 1; for (OrderVO order : orderList) { HSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(order.getOrderNo()); row.createCell(1).setCellValue(order.getProductTitle()); row.createCell(2).setCellValue(order.getBuyerName()); row.createCell(3).setCellValue(order.getSellerName()); row.createCell(4).setCellValue(order.getPrice().doubleValue()); row.createCell(5).setCellValue(order.getStatusDesc()); row.createCell(6).setCellValue(order.getCreateTime()); } response.setContentType(application/vnd.ms-excel;charsetutf-8); response.setHeader(Content-Disposition, attachment;filenameorders_ System.currentTimeMillis() .xls); workbook.write(response.getOutputStream()); }这段代码有几个要点Content-Type要设置成application/vnd.ms-excel而不是默认的text/html否则浏览器可能直接显示乱码Content-Disposition的attachment表示作为附件下载而不是在浏览器内打开文件名加时间戳是为了避免同名文件覆盖。这里选择的HSSFWorkbook生成的是.xls格式毕设场景完全够用如果以后需要处理大数据量再考虑XSSF.xlsx或SXSSF流式写。学会POI的套路之后遇到“导出用户列表”“导出商品列表”都只是换表头换数据源的问题。4. 从“能跑”到“能答辩”页面细节与实战排坑4.1 JSP改了不生效先查这三个地方“JSP改了不生效”是热搜词里的高频问题也是我折腾最久的一个坑。明明代码改对了刷新浏览器还是旧页面第一次遇到时差点以为是灵异事件。后来总结出三个最常见的排查层级第一浏览器缓存。JSP最终是在服务器端渲染成HTML发给浏览器的但浏览器可能会把渲染结果缓存下来。你先试Ctrl F5强制刷新或者打开开发者工具的Network面板勾选Disable cache再刷新。很多时候问题就这么解决了。第二Tomcat的Jasper缓存。JSP第一次被访问时Tomcat会把它编译成Java源码再编译成class文件。你修改JSP文件后如果重新部署时Tomcat没有清掉旧的编译结果就会继续执行旧class。这个缓存通常存在于Tomcat的work目录。解决办法是在IDE里执行“Clean Project”或“Rebuild Project”然后重新部署。IDEA的话按一次CtrlF5还不够大概率要Build - Rebuild Project再重启Tomcat。第三部署目录不对。JSP文件要拷贝到Tomcat实际运行的webapps目录下。IDEA开发时你改的是源码目录下的JSP但Tomcat跑的是target目录下的一份拷贝。如果IDE的项目没有自动同步auto redeploy配置好你改的源码根本不会被复制到运行目录。这个时候去target/目录下看一眼JSP文件是否更新了就能定位问题。还有一个经验把JSP放在WEB-INF/views下开发时部分IDE在热部署方面确实不如放在webapp根目录下响应快。如果你遇到“改了必须重启才生效”的情况可以先配置一下Tomcat的on frame deactivation为Update classes and resources这能明显改善开发体验。4.2 图片上传与MP4视频播放的两个坑图片上传和展示最常见的坑就是“数据库里存的路径访问不到”。我刚开始做的时候把图片写到了项目运行目录下的临时目录里然后页面里用img src/upload/xxx.jpg去访问结果显示404。问题在于Tomcat默认的静态资源处理器并不知道/upload/这个URL应该映射到磁盘的哪个物理目录。解决方案有两种。第一种是在springmvc.xml里配置虚拟映射mvc:resources mapping/upload/** locationfile:E:/project/upload//第二种是推荐的做法如果图片就在webapp/upload目录下那么用mvc:resources mapping/upload/** location/upload//放行即可。要注意location结尾的斜杠不能省否则Spring匹配路径时会出问题。另外所有页面中涉及资源URL的地方推荐统一使用${pageContext.request.contextPath}来拼接上下文路径。比如img src${pageContext.request.contextPath}${product.coverImage} /否则部署时项目访问名一旦不是根路径图片全部会挂掉。关于热搜词里“jsp实现mp4视频播放”我是在做一个扩展功能时遇到的。如果商品详情页要支持展示介绍视频常用的做法是HTML5的video标签video width640 height360 controls src${pageContext.request.contextPath}/upload/${product.videoUrl}/video但是我把MP4文件放到upload目录后浏览器并没有像预期那样播放而是直接弹出了下载框。原因是Tomcat默认的web.xml里并没有注册.mp4对应的MIME类型服务器返回的Content-Type不是video/mp4。解决方法是修改Tomcat的conf/web.xml或者在项目的web.xml里添加mime-mapping extensionmp4/extension mime-typevideo/mp4/mime-type /mime-mapping这个问题提醒我们一个通用的排查思路浏览器能不能正确显示文件取决于服务器响应头里的Content-Type遇到文件在浏览器里“打开”与“下载”行为与预期不符时第一反应应该是查MIME映射而不是调前端代码。4.3 并发下单的一致性保护乐观锁与唯一索引前面讲下单逻辑时已经提到了乐观锁这里再展开讲一个单独的场景两个同学同时看中同一件商品同时点了“立即购买”。如果代码只是先查商品状态是0再执行更新那么两个请求都可能查到status0然后都执行成功生成两笔订单一件商品被卖出两次。这是一个典型的数据一致性问题真实交易系统绝对不能接受。我的解决方案是双保险第一层保险是“条件更新”的乐观锁也就是前面代码里的UPDATE product SET status 1 WHERE id ? AND status 0。MySQL执行这条UPDATE时会自动对命中的行加锁不管同时来了多少个请求最终只有一个请求能拿到行锁并把status从0改成1其他请求的UPDATE影响行数为0从而在业务层判定失败。第二层保险是数据库层面的唯一索引也就是订单表里UNIQUE KEY uk_product_id (product_id)。这保证即使未来业务代码被改坏出现了第一个保险失效的情况比如某个新同事用一个不带条件状态的UPDATE把商品update了数据库也会因为product_id重复而拒绝插入第二笔订单。表结构层面的约束是最后一道底线。这个“业务层乐观锁 数据库层唯一索引”的纵深防御思想在答辩时是非常亮眼的点。老师大概率会追问“你们怎么处理多人同时购买的问题”你把这个双保险的来龙去脉讲清楚基本上这个问题就稳了。4.4 答辩前值得加的五个小细节我要说一个残酷又现实的经验很多功能不齐全但细节到位的项目得分往往高于功能堆得多但粗糙的系统。如果你的时间是有限的我强烈建议把以下五个细节优先级提前。第一分页。用PageHelper插件三行代码就能让列表页从前台一次性全量加载变成真正的分页。这不只是性能问题更是一种“项目规范感”的体现。第二登录拦截器。用SpringMVC的HandlerInterceptor实现定义登录拦截器把/product/add、/order/create、/user/center等需要登录的请求拦截住未登录时重定向到登录页。这是Web应用的基本安全能力。第三统一异常处理。用ControllerAdvice加ExceptionHandler处理全局异常捕获到的异常记录日志并返回友好提示而不是让Tomcat抛出一整页500错误堆栈。完整异常体系是评判系统“成熟度”的重要维度。第四MyBatis的#{}参数绑定。写Mapper XML时严禁用${}拼接SQL因为${}是直接字符串替换存在SQL注入风险#{}会被预编译成占位符天然免疫大部分注入攻击。能把这个原理讲清楚说明你对安全有基本认知。第五后台数据可视化。不需要做得花里胡哨只要在管理后台首页展示几个统计数字今日新增用户数、在售商品数、已完成订单数、成交总额。用一行COUNT聚合SQL就能完成但视觉上的“系统感”却会强很多。最后再说几句体己话整个项目从零开始做到全部收尾我最大的体会不是代码量有多大而是“想清楚再做”这件事的价值。数据库字段为什么这样设计状态为什么用数字不用字符串订单里为什么要冗余成交价格并发下单为什么必须用条件更新——这些看似微小的决策恰恰构成了你在答辩时能够从容表达的核心素材。很多同学被答辩老师问住往往不是因为代码功能不对而是因为他从来没有自己问过自己“为什么这样写”。另外这块平台做完之后别急着删。把闲置物品发布、下单、订单状态流转这条主线再走一遍把Excel导出、视频展示、统计看板这些扩展功能单独整理成一篇文档它们就是你简历上“项目经验”一栏最真实的内容。月后面如果再有人问我“JSP这么老学它还有用吗”我会告诉他老只是相对技术迭代而言你在SSM里练过的分层思想、事务边界、并发控制和数据库设计换任何框架都不会过时。
返回列表