ARTICLE DETAIL

资讯详情

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

JSP+Servlet蛋糕店网站实战:从架构设计到事务实现

JSP+Servlet蛋糕店网站实战:从架构设计到事务实现 简介一份基于JSP和Servlet的JavaWeb蛋糕店售卖网站项目源码适用于JavaWeb学习者、毕业设计或课程设计场景项目采用IDEA 2017.3.5开发Tomcat 8.5.35MySQL 5.7包含前台购物与后台管理两大模块。前台实现商品推荐条幅、热销、新品、分类展示、搜索、购物车、登录注册、订单支付与查询以及个人信息与收获地址修改等功能后台支持订单状态管理、用户管理、商品类目维护及商品信息增删改覆盖电商网站核心流程。资源包共290个文件含55个Java源码、58个JSP页面、20个XML配置、9个JAR依赖库以及SQL脚本、CSS/JS等前端资源压缩包约19.11MB项目采用c3p0连接池和DButil工具封装数据库脚本可直接导入MySQL目录结构清晰便于导入IDE部署运行。已有127人浏览学习适合需要完整电商案例参考、二次开发或理解JavaWeb分层架构的开发者。1. 为什么一个 JSPServlet 蛋糕店网站依然值得亲手做一遍学校旁边那家蛋糕店老板天天在朋友圈发新品图顾客在评论区排队下单最后结账全靠手抄。用 JSP 和 Servlet 搭一个 JavaWeb 蛋糕店售卖网站就是把这套流程搬到浏览器里顾客自己看蛋糕、加购物车、下单老板在后台看到订单改状态。这个题目几乎是 JavaWeb 课设里出现频率最高的一类因为它把 Servlet 做控制器、JSP 做页面、MySQL 做存储这套经典组合用得最完整也把注册登录、商品列表、购物车、下单事务这些后端的常见考点全串起来了。这篇笔记把它拆到能照着复现的程度适合正在做课设、准备毕设、或者想补 JavaWeb 基本功的人。2. 技术选型与工程结构JSPServlet 的蛋糕房怎么搭骨架2.1 一次完整请求的数据流从点草莓蛋糕到页面渲染先把最核心的链路看懂后面所有功能都是它的变体。顾客在首页点下“草莓蛋糕”浏览器发出一个 GET 请求/cake/detail?id3Tomcat 根据WebServlet注解找到对应的 Servlet。Servlet 不画页面它只负责拿参数、调业务、把结果存进 request 作用域再转发给 JSP 去渲染。这条链路上有一个关键动作request.getRequestDispatcher(xxx.jsp).forward(request, response)。forward 是服务端内部跳转地址栏不变request 里塞的对象在 JSP 里可以直接用${cake.name}取出来。另一个动作是sendRedirect它会让浏览器重新发一次请求地址栏会变适合下单成功之后跳到结果页。为什么要区分这两个跳转因为涉及到表单重复提交的坑第 5 章会专门讲。再往下是数据层。Servlet 调用 serviceservice 调用 daodao 执行 JDBC 查询返回实体对象。这个四层结构在蛋糕店这种小项目里看似笨重但它是 Spring MVC 的雏形。你翻黑马的 JavaWeb 笔记Servlet 生命周期和请求转发那几节值得反复看因为你之后遇到的 404、乱码、重复提交全是这条链路上某个环节出了岔子。2.2 分层结构把蛋糕店网站按职责拆开一个能跑且能答辩的蛋糕店项目我习惯按 controller、service、dao、entity 四层拆包。不要把所有代码堆进 Servlet那叫面条代码能跑但被老师问两句就露馅。cake-shop/ ├── src/main/ │ ├── java/com/cake/shop/ │ │ ├── controller/ # Servlet 控制器只做参数解析和页面跳转 │ │ │ ├── UserServlet.java │ │ │ ├── CakeServlet.java │ │ │ ├── CartServlet.java │ │ │ └── OrderServlet.java │ │ ├── service/ # 业务逻辑下单事务、库存扣减、订单号生成 │ │ ├── dao/ # 数据访问每张表对应一个 Dao │ │ ├── entity/ # 实体类User、Cake、Order、OrderItem │ │ ├── filter/ # 过滤器字符编码、登录拦截 │ │ └── util/ # JDBC 工具类、MD5 工具类 │ └── webapp/ │ ├── jsp/ # 视图页面login.jsp、register.jsp、index.jsp │ ├── static/ # css、js、images、fonts │ ├── index.jsp │ └── WEB-INF/web.xml └── pom.xml每个包的职责一句话说清controller 拿到参数后不做业务判断只调 service 并决定跳哪个页面service 管业务规则比如购物车为空不能下单、下单要开事务dao 只写 SQLentity 是表的映射。蛋糕店项目虽然不大但强行分层的收益很大——老板哪天说“蛋糕要按销量排序”你只需在 service 加一个方法完全不用动 JSP 和 Servlet。2.3 IDEA 运行 JavaWeb 项目的最小配置这一步不过关后面全是 404。用 IDEA 跑 Tomcat 的流程是新建或者打开一个 JavaWeb 工程然后 Run → Edit Configurations点左上角加号选 Tomcat Server → Local。在 Deployment 页签里点加号把 Artifact 选为war explodedApplication context 设置成/cake_shop。为什么不选 warwar 是压缩包IDEA 每次改完代码都要重新打包再部署改一个 JSP 要等几十秒war exploded 直接把 webapp 目录映射给 TomcatJSP 改完刷新浏览器就生效。开发体验完全不一样。这里还有个小坑Maven 工程里 servlet-api 的 scope 要写成 provided因为 Tomcat 自带了 servlet-api你打包再带一份会冲突。JSTL 依赖不能漏漏了 JSP 里的c:forEach标签直接报错。跑通之后访问http://localhost:8080/cake_shop/看到首页就说明环境通了。这个配置是所有 JavaWeb 项目的地基值得在正式开始写代码前花半小时搞定。3. 数据库设计与数据访问层先建表再写代码3.1 五张核心表用户、分类、蛋糕、订单、订单明细先设计表再写 Java 代码是我做过所有课设后最不后悔的顺序。蛋糕店网站的数据量不大但表关系很典型完整建表 SQL 如下-- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5加盐后存储, phone VARCHAR(20) COMMENT 收货电话, address VARCHAR(255) COMMENT 默认收货地址, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 如生日蛋糕、慕斯、小甜点 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 蛋糕表 CREATE TABLE cake ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT COMMENT 关联分类, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 单价精确到分, image VARCHAR(255) COMMENT 图片URL, description TEXT COMMENT 配料和口味描述, stock INT DEFAULT 0 COMMENT 库存, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, KEY idx_category (category_id), CONSTRAINT fk_cake_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单号时间戳用户ID, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成 4已取消, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, cake_id INT NOT NULL, cake_name VARCHAR(100) COMMENT 下单时蛋糕名称快照, price DECIMAL(10,2) COMMENT 下单时单价快照, quantity INT NOT NULL COMMENT 数量, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里没有设计购物车表。购物车是临时数据生命周期等于浏览器会话用 Session 存最合理用户关掉浏览器购物车就清空不需要频繁写数据库。只有下单那一刻才把购物车内容落到订单表。这个设计选择可以在答辩时主动讲会显得你真的思考过。3.2 订单表的两个设计细节快照与金额类型订单明细表里为什么存cake_name和price快照而不是下单时再去关联蛋糕表因为蛋糕价格会变。今天 98 元的草莓蛋糕明天促销改成 88 元如果订单明细只存 cake_id历史订单的金额就跟着漂移了。快照字段保证每一笔订单都定格在当时的价格和品名上这是电商订单设计的常识。金额字段用DECIMAL(10,2)而不是double因为 float/double 在二进制里无法精确表示十进制小数算总价时会出现 0.10.2 ! 0.3 这种翻车现场。DECIMAL 是字符串存储精度由你指定适合存钱。这个细节放进答辩能省不少提问。另一个容易被问的是外键。互联网大厂现在普遍不用物理外键因为分库分表后外键约束会成为性能瓶颈。但课设项目用外键是加分项课程里讲的就是完整性约束这里用FOREIGN KEY能体现你理解了表关系。注意订单明细表的ON DELETE CASCADE删除订单主记录时明细自动删除省得写递归删除。3.3 用 Druid 连接池和 BaseDao 模板封装数据访问每个 Servlet 都去DriverManager.getConnection是低效的连接用完就关高并发时频繁创建销毁连接数据库会扛不住。我用 Druid 连接池在src/main/resources下放一个druid.properties# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/cake_shop?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 maxWait3000 minIdle2参数含义要心里有数initialSize 是启动时预建的连接数maxActive 是最大活跃连接数课设 20 并发足够maxWait 是拿不到连接时的最长等待毫秒数超过就抛异常避免线程无限阻塞。连接串里的characterEncodingutf8mb4和serverTimezoneAsia/Shanghai分别是治乱码和时区偏差的缺一个都可能在特定环境踩坑。工具类封装成单例整个项目共用一份public class JdbcUtil { private static DruidDataSource dataSource; static { try (InputStream in JdbcUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }再加一个 BaseDao 把通用的增删改查抽出来。核心是update(String sql, Object... args)方法用 PreparedStatement 而非 Statement防止 SQL 注入。PreparedStatement 会预编译 SQL参数用?占位用户输入的值永远只被当作字面量不会被拼进 SQL 变成攻击语句。这一步是安全底线答辩必问。public class BaseDao { public int update(String sql, Object... args) { try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i args.length; i) { ps.setObject(i 1, args[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } } }实体类不用贴代码就是private字段加 getter/setter对应每张表。User 对应 user 表Cake 对应 cake 表Order 里加一个ListOrderItem items字段表示一个订单包含多个明细这样 JSP 里遍历订单和明细会很顺畅。4. 核心功能实现从注册登录到购物车下单4.1 用户注册与登录Servlet 接收表单Session 保存登录态先写登录 Servlet。这段代码是整个网站安全控制的地基登录态保存不下来购物车和下单都无从谈起。WebServlet(/login) public class UserServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 必须在读取任何参数之前设置编码否则中文用户名直接乱码 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); UserService userService new UserService(); User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); } else { req.getSession().setAttribute(user, user); resp.sendRedirect(req.getContextPath() /index); } } }代码逻辑分三条分支登录失败把错误信息放进 request 并 forward 回登录页JSP 里用${error}显示登录成功把 user 对象放进 Session然后用 sendRedirect 跳回首页。这里刻意把“失败用 forward、成功用 redirect”区分开是为了避免刷新时表单重复提交详情见第 5 章。密码不能明文存数据库。常见做法是 MD5 加盐注册时对password salt做 MD5登录时同样拼接后比较public static String md5WithSalt(String password, String salt) { String input password salt; return DigestUtils.md5Hex(input); }JSP 登录页注意一点form 的 action 不要写死/login用 EL 表达式拼上下文路径form action${pageContext.request.contextPath}/login methodpost input typetext nameusername placeholder用户名 required input typepassword namepassword placeholder密码 required button typesubmit登录/button c:if test${not empty error} p stylecolor:red${error}/p /c:if /form这样网站以后部署到别的路径下表单依然能提交对位置。注册逻辑类似加一个“用户名查重”的步骤如果已存在就返回提示不存在才插入数据库。4.2 蛋糕列表与详情JSTL 循环渲染加简单分页首页蛋糕列表是访客看到的第一屏用 JSTL 的c:forEach循环渲染卡片。先写 ServletWebServlet(/index) public class CakeServlet extends HttpServlet { private CakeService cakeService new CakeService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNo Integer.parseInt( req.getParameter(pageNo) null ? 1 : req.getParameter(pageNo)); int pageSize 8; ListCake cakeList cakeService.findPage(pageNo, pageSize); int total cakeService.count(); int totalPage (int) Math.ceil(total * 1.0 / pageSize); req.setAttribute(cakeList, cakeList); req.setAttribute(totalPage, totalPage); req.setAttribute(pageNo, pageNo); req.getRequestDispatcher(/jsp/index.jsp).forward(req, resp); } }分页的 SQL 是SELECT * FROM cake WHERE status 1 LIMIT ?, ?limit 第一个参数是偏移量(pageNo - 1) * pageSize第二个是每页条数。count 用SELECT COUNT(*) FROM cake WHERE status 1。两个查询各执行一次数据量小时没问题。JSP 渲染代码% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classcake-grid c:forEach items${cakeList} varcake div classcake-card img src${cake.image} alt${cake.name} h3${cake.name}/h3 p${cake.price}/p a href${pageContext.request.contextPath}/cake/detail?id${cake.id}查看详情/a /div /c:forEach /divitems是 Servlet 塞进 request 的 Listvarcake是循环变量每次迭代取一个 Cake 对象${cake.name}自动调用 getName()。JSTL 的便利之处在于没有 Java 代码也能完成循环和条件判断JSP 页面保持干净。4.3 购物车与下单Session 存车事务落库购物车用 Session 里的MapInteger, Integer存key 是蛋糕 IDvalue 是数量。加购接口WebServlet(/cart/add) public class CartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int cakeId Integer.parseInt(req.getParameter(cakeId)); HttpSession session req.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } cart.put(cakeId, cart.getOrDefault(cakeId, 0) 1); resp.sendRedirect(req.getContextPath() /index); } }首次访问时 Session 里没有 cart先建一个空 Map 放进去再累加数量。getOrDefault处理已有商品追加数量的情况比containsKey加get再put简洁得多。下单是整个项目里最需要认真对待的事务操作因为要同时写订单主表和明细表任何一条失败都不能留下半截订单public String createOrder(int userId, MapInteger, Integer cart, String name, String phone, String address) { String orderNo System.currentTimeMillis() _ userId; Connection conn null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交手动控制事务 // 1. 插入订单主表 String insertOrder INSERT INTO orders(order_no, user_id, total_price, status, receiver_name, receiver_phone, receiver_address) VALUES(?,?,?,0,?,?,?); PreparedStatement ps conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); // 计算 totalPrice遍历 cart 用 cakeService 查价格累加 ps.setString(1, orderNo); ps.setInt(2, userId); // ps.setBigDecimal(3, totalPrice) ... ps.executeUpdate(); ResultSet keys ps.getGeneratedKeys(); int orderId 0; if (keys.next()) { orderId keys.getInt(1); } // 2. 逐条插入明细 String insertItem INSERT INTO order_item(order_id, cake_id, cake_name, price, quantity) VALUES(?,?,?,?,?); for (Integer cakeId : cart.keySet()) { Cake cake cakeService.findById(cakeId); PreparedStatement ps2 conn.prepareStatement(insertItem); ps2.setInt(1, orderId); ps2.setInt(2, cakeId); ps2.setString(3, cake.getName()); ps2.setBigDecimal(4, cake.getPrice()); ps2.setInt(5, cart.get(cakeId)); ps2.executeUpdate(); } // 3. 扣减库存UPDATE cake SET stock stock - ? WHERE id ? conn.commit(); return orderNo; } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这里最核心的是conn.setAutoCommit(false)和最后的conn.commit()。MySQL 默认每条 SQL 自动提交如果插入主表成功、明细失败就会出现一个没有明细的空订单。关掉自动提交后只有明细全部插入成功才统一 commit任何一步出错都 rollback 回滚。事务是后端的高频考点这个案例能讲清楚为什么需要它。下单成功后Servlet 要清空 Session 里的购物车并跳转到成功页使用 PRG 模式防止重复提交req.getSession().removeAttribute(cart); resp.sendRedirect(req.getContextPath() /order/success?orderNo orderNo);4.4 后台订单管理过滤登录拦截表格展示订单后台模块给蛋糕店老板用列出所有订单并允许修改状态。先做一个登录拦截过滤器只有管理员能访问/admin/*路径WebFilter(/admin/*) public class AdminFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); User user session null ? null : (User) session.getAttribute(user); if (user null || !admin.equals(user.getUsername())) { resp.sendRedirect(req.getContextPath() /jsp/login.jsp); return; } chain.doFilter(request, response); } }过滤器在所有 Servlet 之前执行拿不到登录态直接重定向到登录页。后台订单列表的 Servlet 和 JSP 与前面的登录逻辑类似只是查询换成SELECT * FROM orders ORDER BY id DESCJSP 用表格展示订单号、下单人、总价、状态状态列提供“发货”“完成”的跳转链接指向OrderStatusServlet更新状态字段。整个后台不需要特别复杂但要保证没有登录态的人在浏览器直接输入/admin/order/list时进不去。5. JavaWeb 课设最容易踩的 5 个坑与排查方法这一章全部来自血泪经验每一条都是“现象 → 原因 → 解决”三步讲清。5.1 页面和数据库全是问号现象注册“张三”页面回显正常但数据库里存的是“???”或者 JSP 页面中文正常Servlet 读参数读出来是乱码。原因三层编码不一致。POST 请求的请求体编码由request.setCharacterEncoding决定而且必须在第一次读取参数之前设置JDBC 连接串没指定字符编码MySQL 表用了默认的 latin1 字符集。解决三层一起改。JSP 顶部写pageEncodingUTF-8所有 Servlet 的 doPost 第一行执行req.setCharacterEncoding(UTF-8)更省事的方式是写一个CharacterEncodingFilter对所有请求统一设置 UTF-8JDBC 连接串加useUnicodetruecharacterEncodingutf8mb4建表语句显式写DEFAULT CHARSETutf8mb4重新建表而不是只改表的某个字段。5.2 刷新一下页面订单多了一倍现象下单成功后顾客习惯性刷新一下页面后台出现两个一模一样的订单。原因下单请求是 POST浏览器刷新时会重新发送上一次的 POST 请求。Servlet 没有做幂等处理同一个购物车内容被提交了两次。很多同学为了解决“下单后页面显示旧数据”的问题在 JSP 里用 onload 事件刷新页面结果把重复提交问题放大了。解决使用 PRG 模式下单成功后不要 forward而是sendRedirect到 GET 成功页。浏览器地址栏变成/order/success?orderNoxxx刷新这个 GET 请求不会触发下单逻辑。如果还想更严谨可以在表单里埋一个一次性 token提交时从 Session 取出比对比对成功后立即删除重复提交时 token 已失效直接拒绝。5.3 IDEA 启动 Tomcat 后访问 404现象Tomcat 日志显示启动成功但访问http://localhost:8080/cake_shop/报 404或者显示的是 Tomcat 默认页面而不是你的项目。原因Run Configuration 的 Deployment 页签里没有添加 Artifact或者添加的是 war 包而不是 exploded 目录Application context 设置成了/也可能是依赖里的 servlet-api 和 Tomcat 自带的冲突导致启动异常但日志不明显。解决打开 Run → Edit Configurations → 选中你的 Tomcat → Deployment → 点加号 → Artifact → 选xxx:war exploded把 Application context 改成/cake_shopApply 后重启。如果端口被占用看日志里的Port already in use找到占用 8080 的进程结束掉。5.4 JSP 页面的${cake.name}原样输出现象页面上直接显示${cake.name}没有渲染成蛋糕名称或者c:forEach标签不生效。原因web.xml 里的 web-app 版本低于 2.5JSP 默认isELIgnoredtrueEL 表达式被当作文本输出或者 JSTL 依赖没引入标签解析失败。解决确认 web.xml 使用的是 3.0 以上的版本声明不在前端手动设置isELIgnoredtrueMaven 里引入 JSTL 依赖并在 JSP 第一行写全 taglib 指令% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。检查 web.xml 版本IDEA 里打开WEB-INF/web.xml看web-app根标签的版本属性是最快的方法。5.5 图片上传成功但重启后全丢了现象后台管理上传蛋糕图片当时预览正常重启 Tomcat 或者重新构建项目后图片全部 404 红叉。原因开发环境用 war exploded 部署上传的图片被写进了项目的 target 目录IDEA 重新构建时这个目录会被清理。写在 Tomcat 的 webapps 下的文件重新部署也会被删除。解决把上传的文件放到项目外部用 Tomcat 虚拟路径映射访问。在 Tomcat 的conf/server.xml或独立 context 里配置Context docBaseD:/cake_shop_upload path/upload /上传时保存到 D 盘绝对路径页面上用/upload/xxx.jpg访问。项目重建、Tomcat 重装图片都还在。注意 docBase 指向的真实目录必须存在否则 Tomcat 启动直接报错。6. 进阶技巧与验证让蛋糕店网站更耐看更禁得起问6.1 图片坐标定位在蛋糕图上做交互标记“jsp 图片如何对坐标定位”是新手常搜的问题常见场景是蛋糕详情页想在大图上的“水果夹层”“奶油裱花”位置加文字标记。最省事的方案不是 Java 代码而是 CSS 相对定位图片父容器position: relative标记元素position: absolute坐标用百分比而不是像素这样图片等比缩放后标记不会乱跑。div styleposition:relative; display:inline-block; img src${cake.image} alt${cake.name} stylewidth:400px; height:300px; span styleposition:absolute; left:25%; top:60%; background:rgba(255,0,0,0.7); padding:2px 6px; font-size:12px; color:#fff;水果夹层/span /div设置坐标的依据是图片的实际宽高比如标记想放在水平 25% 和垂直 60% 的位置就写left:25%; top:60%。个人信息展示页要用同样的技巧做“VIP 角标”把头像容器设 relative角标 absolute 定位到右上角即可。记住一个规则js 负责定位的属性永远放在 img 的父容器而不是 img 本身上。6.2 JSP 页面引入 Element 图标不引整套 Vue很多人想在 JSP 页面用饿了么 Element 的图标但 Element 是 Vue 组件库JSP 里用不了组件。其实图标库本身是纯 CSS 字体文件不必引 Vue 全家桶。把 Element 的index.css和fonts目录放进webapp/staticJSP 里引入之后就能直接写i classel-icon-edit/i。link relstylesheet href${pageContext.request.contextPath}/static/element/index.css i classel-icon-shopping-cart/i 我的购物车字体文件是图标渲染的关键只拷 index.css 不拷 fonts 目录图标会显示成方块这是最常见的翻车点。自己在本地下载一次资源之后所有页面都能用不需要联网加载。6.3 验证方法从功能测试到并发冒烟交项目前至少按这个清单跑一遍注册新用户 → 登录 → 加购三种以上蛋糕 → 下单 → 查看订单状态 → 后台改状态 → 用第二个账号重复一遍。重点验证边界场景购物车为空时点下单页面是否给出友好提示库存不足时扣减是否会变成负数订单详情页刷新多次后台订单数量是否保持不变。条件允许的话用 JMeter 模拟 20 个并发用户同时下单观察 Druid 连接池的 maxActive 是否够用日志有无连接超时。这套流程跑完能躲掉答辩时八成功能性问题。我在做第一个 JavaWeb 项目时下单后直接 forward 到成功页结果刷新两次后台出现了三份订单被老师当场指出来。后来老老实实把 PRG 和事务写完整才理解这两个机制不是书上考点而是真实业务防止数据翻车的底线。你把这个蛋糕店网站做完别急着交先按上面的流程自己折腾一遍踩过的坑都会变成答辩时的底气。希望帮到你。本文还有配套的精品资源点击获取
返回列表