ARTICLE DETAIL

资讯详情

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

Java网上商城系统设计与实现:从购物车到支付回调的完整实战

Java网上商城系统设计与实现:从购物车到支付回调的完整实战 简介一份基于Java技术栈的网上商城网站设计与实现文档面向Java Web方向毕业生、电商项目开发人员及需要参考完整设计流程的初学者。它以零食在线销售为业务场景系统梳理了课题背景、可行性分析、需求分析、总体设计等章节并围绕BS架构、JSP动态页面、MySQL数据库与MyEclipse工具链给出具体技术方案可直接用于毕业设计论文撰写和系统开发起步。压缩包仅含1个docx文件大小约1.23MB内容以论文正文为主结构清晰、便于按章节查阅和二次编辑。目前已有188人浏览学习说明其具有一定参考价值。文档详细覆盖了用户管理、按分类/新品/特价等维度的商品检索、购物车与订单处理、第三方支付接口对接、物流追踪、在线客服等功能模块还给出业务流程图、数据库设计思路和系统优势分析并针对订单流程补充了库存检查、价格计算、优惠券应用等关键细节可帮助读者快速复现同类商城项目也能为毕业答辩和后续扩展提供扎实的论述素材。1. 基于 Java 的网上商城不是增删改查是从加购到发货的一条完整链路拿到“基于java网上商城网站设计与实现”这类题目时最常见的误区是把它当成三张表加几个页面的 CRUD用户表、商品表、订单表页面能跳就算做完。但真把商城当成品交付时难点几乎都集中在购物车与库存、下单事务和支付回调这几段而不是页面本身。这篇文章按 Java 技术栈从技术选型、数据库建模、核心流程实现到部署避坑完整走一遍适合正在做毕业设计、课程设计或者想用第一个完整项目练手的 Java 学习者。你不需要预先会微服务能跑通 Spring Boot 就够了。2. 技术选型先想清楚Spring Boot MyBatis 为什么是商城最稳的底子2.1 不用 SSH 也不用纯 JSP理由集中在维护成本很多教程还在讲 Struts2 Spring Hibernate但这套组合在 2024 年已经不是新项目的首选。Struts2 早年出过多次远程代码执行漏洞官方社区活跃度也低新手照着老教程写完光解决依赖冲突就要耗掉一整天。纯 JSP Servlet 不是不能做而是购物车、事务管理、登录拦截这些都要手写等于把 Spring 容器帮你做好的事重新做一遍工作量翻倍还不一定写得对。Spring Boot MyBatis 的组合胜在三个地方一是起步依赖把 Tomcat、Jackson、参数校验这些常用件都带好了写一个 Controller 就能跑起来二是 MyBatis 保留了手写 SQL 的能力商城这种要写多表 join、行级锁、条件更新的场景SQL 在手上有明确的调优抓手三是生态成熟网上能搜到的“网上商城 用例图、源码、部署教程”绝大多数都是这个技术栈遇到问题搜一搜就有答案。JPA 确实更省代码但对新手像个黑匣子复杂查询翻车以后反而不容易定位。2.2 最小可运行骨架pom.xml 与 application.yml 各配什么建一个 Spring Boot 工程第一步是确定依赖。下面这份 pom.xml 是我常用的最小集去掉了无关插件parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里只引了 web、MyBatis、MySQL 驱动和 Lombok。Lombok 不是必须的但能省掉 getter/setter 的重复代码对后期维护友好。版本 2.7.18 是 Spring Boot 2.x 后期比较稳的一个版本JDK 8 和 JDK 11 都能跑比直接上 3.x 少很多环境问题。MyBatis 的 starter 版本要和 Spring Boot 版本匹配如果 boot 升到 3.xmybatis starter 也要换对应版本。再配 application.ymlserver: port: 8080 servlet: context-path: /shop spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.shop.entity configuration: map-underscore-to-camel-case: trueurl 里必须带serverTimezoneAsia/Shanghai否则 MySQL 8 会报时区错误。map-underscore-to-camel-case打开后数据库字段create_time能直接映射到 Java 属性createTime省掉大量 resultMap。context-path配成 /shop 后所有接口都要通过http://localhost:8080/shop/...访问这个前缀和后面部署有关系提前想好能少踩一次 404 的坑。2.3 controller/service/mapper 分包一个能长期改动的工程结构商城项目后期一定会加功能比如优惠券、秒杀、多商户所以包结构从第一天就按职责分好。常见做法是 controller、service、mapper、entity、config、common 六层com.example.shop ├── controller # 接收参数、返回结果 ├── service # 业务逻辑事务边界在这里 ├── mapper # MyBatis 接口只写数据库操作 ├── entity # 数据库表对应的实体 ├── config # 拦截器、跨域、静态资源配置 └── common # 统一返回体、异常、工具类有个容易翻车的点是事务边界。事务注解Transactional要放在 service 实现类上而不是 controller 上。controller 只做参数接收和结果封装一旦把事务放上去多个请求同时进来时事务范围被拉长连接池很快会被占满。service 里一个方法就是一次业务操作的完整事务这个习惯从写第一个下单接口时就要养成。3. 数据库设计先定边界SPU/SKU、库存与订单明细3.1 SPU 与 SKU拆不拆取决于商品要不要多规格做商城数据库设计先回答一个问题商品要不要支持多规格。比如一件 T 恤有红色和蓝色两个 SKU库存是各自独立的。如果只需要单规格把商品和库存合成一张表可以少写很多 join但后期加规格就得改表结构相对麻烦。多商户跨境商城那种真实项目几乎都会拆 SPU标准产品单元和 SKU库存量单位两张表这是 Java 商城源码里最常见的建模方式。我的建议是直接拆成本并不高CREATE TABLE spu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, spu_no VARCHAR(32) NOT NULL COMMENT 商品编号, title VARCHAR(128) NOT NULL COMMENT 商品标题, category_id BIGINT NOT NULL COMMENT 分类ID, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sku ( id BIGINT PRIMARY KEY AUTO_INCREMENT, spu_id BIGINT NOT NULL, sku_no VARCHAR(64) NOT NULL, spec VARCHAR(64) NOT NULL COMMENT 规格描述如 红色/X, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意 sku 表里除了 stock还加了一个 version 字段这是为后面扣库存的乐观锁准备的。如果用UPDATE sku SET stock stock - 1 WHERE id ? AND stock 0这种条件更新version 不是必须的但如果你想在更新时做更多校验version 能给你一条退路。两张表通过 spu_id 关联查询商品列表时再 join 分类表前期不建外键性能更好。3.2 订单主表与订单明细为什么必须冗余快照订单表是商城最重要的表设计上有一个新手常忽略的点订单明细必须把下单那一刻的商品信息冗余进去包括标题、图片、单价、规格。因为商品价格和标题随时会改甚至可能下架如果你下单之后去查商品表取名称和价格历史订单就会跟着变对账的时候数据就对不上。订单主表一条记录对应一次下单订单明细表多条记录对应这个订单里的多个商品。两张表用 order_no 关联。订单主表的关键字段如下字段类型说明idBIGINT自增主键不对外暴露order_noVARCHAR(32)业务订单号对外展示、支付回调都用它user_idBIGINT下单用户total_amountDECIMAL(10,2)订单总金额单位为元statusTINYINT10待支付 20已支付 30已发货 40已完成 50已取消pay_timeDATETIME支付时间create_timeDATETIME下单时间订单明细表里要有sku_id、sku_spec规格快照、product_title标题快照、product_image图片快照、price成交单价快照、count数量。这些字段全部来自下单那一刻的 sku 和 spu 数据之后商品怎么改都与历史订单无关。这一点做到位后面做订单列表、统计报表都不会因为商品变更而翻车。3.3 金额用 decimal主键不暴露外键不建三个小而关键的约定金额字段必须用DECIMAL(10,2)不要用 float 或 double。二进制浮点数在加减时会产生精度误差订单金额差几分钱对不上账这种问题最难排查。Java 侧对应的类型是BigDecimal用new BigDecimal(19.99)的方式构造不要直接传 double。主键用自增BIGINT没问题但对外永远不要暴露自增 id。用户 id 暴露还好订单 id 一旦暴露别人能从自增规律推测出你的订单量这是安全风险。所以订单要有一个独立的order_no用时间戳加随机数生成比如yyyyMMddHHmmss加六位随机数或者用雪花算法生成的字符串。对外展示、支付回调、查询都用 order_no。外键约束建议不建。商城表之间关系复杂外键会导致插入、删除时数据库做额外校验高并发下容易锁竞争。表之间的关系完全由 service 层控制——先查再插、先校验再更新这些逻辑写在 Java 代码里比数据库外键更灵活。4. 从加购到支付回调购物车、下单事务与幂等怎么落地4.1 购物车存 Redis 还是 session两者都行但取舍不同购物车是商城第一个真正有状态的模块。最轻量的做法是存在 session 里用户登录后把购物车对象序列化到 session代码简单不需要额外服务。但 session 方案有两个硬伤一是用户换设备或者清理浏览器缓存购物车就没了二是 session 默认在内存里用户量上来以后内存占用会很难看。真实项目一般用 Redis 存购物车结构上是 hashkey 是用户 idfield 是 sku_idvalue 是购买数量。Redis 的 hash 天然适合购物车这种按用户聚合的场景增加数量、修改规格、清空都很方便。如果只是为了课程设计展示session 方案也能跑通但代码里最好留一个抽象接口后面想切换 Redis 时不用改 controller。Service public class CartService { private final RedisTemplateString, Object redisTemplate; public void addToCart(Long userId, Long skuId, Integer count) { String key cart: userId; // hash 里每个 sku 存一个数量重复加入时累加 redisTemplate.opsForHash().increment(key, skuId.toString(), count); } public MapObject, Object getCart(Long userId) { return redisTemplate.opsForHash().entries(cart: userId); } }increment方法在 hash field 不存在时会从 0 开始加所以重复加购同一个商品不会丢数量。注意购物车里的数量可能超过库存下单时还要再做一次库存校验不能只靠前端限制。Redis 这种方式需要额外部署 Redis 服务教程类项目如果条件受限可以先用 ConcurrentHashMap 模拟或者直接用 session。4.2 下单方法事务里先锁库存再写订单异常一律回滚下单是商城业务里事务性最强的一段。一个下单操作要做的事包括校验购物车、扣减库存、写订单主表、写订单明细表。这四步必须在一个事务里任何一个失败所有已做的修改都要回滚否则会出现库存扣了但订单没建成的数据不一致。下面是一个典型的 service 实现Override Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListCheckoutItem items) { // 1. 生成唯一订单号不用自增id对外暴露 String orderNo generateOrderNo(); BigDecimal totalAmount BigDecimal.ZERO; // 2. 逐条扣减库存条件带上 stock count防止超卖 for (CheckoutItem item : items) { int updated skuMapper.deductStock(item.getSkuId(), item.getCount()); if (updated 0) { throw new StockNotEnoughException(库存不足skuId: item.getSkuId()); } } // 3. 写订单主表 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(10); // 待支付 orderMapper.insert(order); // 4. 写订单明细此时的价格、标题都是快照数据 for (CheckoutItem item : items) { Sku sku skuMapper.selectById(item.getSkuId()); OrderItem detail new OrderItem(); detail.setOrderNo(orderNo); detail.setSkuId(sku.getId()); detail.setProductTitle(sku.getTitle()); // 快照 detail.setPrice(sku.getPrice()); // 快照 detail.setCount(item.getCount()); orderItemMapper.insert(detail); totalAmount totalAmount.add(sku.getPrice().multiply(BigDecimal.valueOf(item.getCount()))); } // 5. 回填总金额再更新一次 orderMapper.updateAmount(orderNo, totalAmount); return order.getId(); }对应的扣库存 SQL 是关键通常写在 mapper XML 里UPDATE sku SET stock stock - #{count} WHERE id #{skuId} AND stock #{count}WHERE stock #{count}是防止超卖的核心。如果库存只剩 2 件两个请求同时来买 2 件数据库行锁会让第二个更新语句返回 0这样就不会出现库存为负的情况。Transactional(rollbackFor Exception.class)必须写因为 Spring 默认只对运行时异常回滚如果你抛的是自定义的 checked 异常不加这个配置事务不会回滚库存照样被扣。事务里的顺序也有讲究先扣库存再写订单。如果先写订单再扣库存库存不足时订单数据已经插入虽然事务会回滚但数据库的 undo log 会多干活锁持有时间变长。先把库存锁住能尽早暴露冲突。4.3 支付回调同一笔通知来三次订单状态只能变一次支付回调是商城最容易写错的一段。第三方支付平台在收到你的应答之前会按一定间隔重发通知可能是同一次支付发三次、五次。如果回调处理逻辑不做幂等用户付一次钱订单状态被重复更新可能会触发两次发货这就是资损级别的 bug。幂等的标准做法是增加一张支付流水表给订单号加唯一约束回调先尝试插入流水插入成功了才去更新订单状态。public PayResult handlePayNotify(PayNotifyRequest notify) { // 1. 先查流水处理过就直接返回成功避免重复业务 PayLog existing payLogMapper.selectByOrderNo(notify.getOrderNo()); if (existing ! null) { return PayResult.success(); } // 2. 尝试插入支付流水唯一索引会挡住并发重复请求 try { PayLog payLog new PayLog(); payLog.setOrderNo(notify.getOrderNo()); payLog.setTradeNo(notify.getTradeNo()); payLog.setAmount(notify.getAmount()); payLogMapper.insert(payLog); } catch (DuplicateKeyException e) { // 并发下另一个请求已经插入成功这里也按成功返回 return PayResult.success(); } // 3. 流水插入成功才更新订单状态为已支付 orderMapper.updateStatus(notify.getOrderNo(), 20, new Date()); return PayResult.success(); }这段代码的关键在于第 2 步的插入。订单号在支付流水表上有唯一索引两个相同回调同时进来时只有一个能插入成功另一个会抛DuplicateKeyException。捕获这个异常后直接返回支付成功因为业务已经被另一个线程处理过了。查了再插、插失败就返回这个模式在任何涉及回调通知的地方都能复用。5. 部署与避坑war 包、JDK 版本和静态资源的三处翻车现场5.1 打 war 还是打 jar取决于你的服务器怎么跑部署方式直接决定你后面会踩哪些坑。Spring Boot 默认打 jar 包用java -jar shop.jar就能启动适合服务器上只跑一个应用的场景。但很多课程设计和公司环境给的是 Windows 服务器上面装好了 Tomcat 8 或者 9要求你把项目打成 war 包丢进 webapps 目录。先改pom.xml把打包方式改成 warpackagingwar/packaging然后启动类继承SpringBootServletInitializer覆盖configure方法。这个步骤漏掉的典型症状是war 包能部署但访问任何接口都是 404。因为 Spring Boot 的自动配置没有被 servlet 容器加载。SpringBootApplication public class ShopApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(ShopApplication.class); } }打包命令统一用mvn clean package -DskipTests打完的产物在target目录下war 包复制到 Tomcat 的webapps目录启动 Tomcat 后会自动解压部署。这里有一个要注意的点如果 application.yml 里配了context-path: /shop访问路径会是http://localhost:8080/shop/...Tomcat 本身不管这个前缀它只负责把请求转给 Spring Boot所以别在 Tomcat 层再配置一遍 context重复配置会导致路径变成/shop/shop。5.2 本地是好的服务器起不来JDK、时区、数据库账号三连问本地跑得好好的复制到服务器上启动失败十有八九是环境差异。第一检查 JDK 版本。Spring Boot 2.7 要求 JDK 8 以上如果你的服务器上装的是 JDK 1.6启动会直接报UnsupportedClassVersionError。用java -version命令确认记住服务器上的 JDK 位数也要看32 位 JDK 跑大应用容易内存不足。第二检查 MySQL 时区。本地 MySQL 可能配置好了serverTimezone但服务器上如果 MySQL 是默认配置启动时 Spring Boot 初始化数据源会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这是时区乱码不是编码问题。最快的解决方式是保持 application.yml 里的 URL 完整包含serverTimezoneAsia/Shanghai并且确认 MySQL 账号有远程访问权限。MySQL 8 默认的认证插件是caching_sha2_password如果你的 Java 驱动版本太老会报认证失败换用 mysql-connector-java 8.0 以上的版本就能解决。第三是端口冲突。服务器上可能已经跑了别的 Java 应用占着 8080启动日志会显示Port already in use。这时候不要急着改代码用netstat -ano | findstr 8080Windows或者lsof -i:8080Linux看看是谁占了端口把配置文件里的server.port改掉或者把冲突进程清掉。5.3 避坑记录五条真实踩坑的现象、原因与解决现象一登录后访问商品列表页面能开但点下单就 404。原因下单接口路径写的是/orders/create但项目配了context-path: /shopcontroller 里用的是相对路径拼接导致实际请求打到/orders/create而不是/shop/orders/create。 解决前端请求统一用绝对路径或者在application.yml里把 context-path 去掉保持部署和本地方案一致。二选一不要两个都留着。现象二图片上传成功但页面上图片裂了。原因文件保存到本地磁盘路径D:/uploads但访问 URL 还是/files/xxx.jpgSpring Boot 默认不映射磁盘物理路径静态资源只在 classpath 下找。 解决加一个 WebMvc 配置类把/files/**映射到本地磁盘目录Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file:D:/uploads/); }Linux 服务器上改成file:/usr/local/shop/uploads/注意结尾必须带斜杠否则映射不生效。现象三同时下单 10 件库存 5 的商品居然下单成功了。原因库存校验写成了select查库存再判断两个线程同时查到库存 5都通过了校验然后一起扣成负数。 解决把校验和扣减合并成一条 update 语句用WHERE stock #{count}做条件更新影响行数为 0 就抛异常。这是数据库层面的行锁比 Java 代码里加 synchronized 靠谱得多。现象四支付成功后订单状态没变但支付流水插进去了。原因支付回调和订单状态更新写在了两个事务里流水插入成功提交了订单更新抛异常回滚两边数据不一致。 解决把订单更新和流水插入放在同一个事务方法里或者先更新订单状态再插入流水顺序调整后要么都成功要么都失败。支付流水表的作用是幂等不是独立事务的起点。现象五服务器上日志全是问号中文全乱。原因Tomcat 的 URI 编码默认不是 UTF-8请求参数里的中文经过 servlet 容器解析后已经变成乱码。 解决在 Tomcat 的server.xml里给Connector加URIEncodingUTF-8同时确认代码里HttpServletResponse的 contentType 设了text/html;charsetUTF-8。这个问题在本地开发时很少出现因为 IDE 里的 Tomcat 通常已经配好了。6. 验证商城没写崩并发压测、回调重放与日志定位最后这一步很多人不做但恰恰是区分“能跑”和“敢上线”的分界线。用压测工具先打一遍核心接口再手动重放支付回调确认关键状态逻辑没有问题这比写多少单元测试都直观。压测用 Apache ab 就够不用上 JMeter。比如验证商品列表接口在 50 并发下能不能扛住ab -n 1000 -c 50 http://localhost:8080/shop/products重点看两个指标Failed requests是不是 0Requests per second是多少。如果失败率超过 5%优先检查是不是连接池配置太小spring.datasource.hikari.maximum-pool-size默认 10并发上来以后线程会等连接表现为接口响应时间暴涨。回调重放测试更简单把支付成功的通知请求用 Postman 手动发两次第二次返回的结果应该和第一次一样而且订单状态在数据库里只变一次。我习惯在回调入口加一行日志记录每次请求的 orderNo 和 tradeNo重放时看日志就能确认幂等是否生效。最后一招是看日志定位问题。部署到服务器上的项目不要用System.out.println输出业务信息统一用 logback把滚动日志配置好logging: level: com.example.shop.mapper: debug把 mapper 层日志级别调成 debug 后MyBatis 会打印实际执行的 SQL 和参数。以后线上出现问题先看日志里最后一条 SQL 执行到什么位置基本能定位是数据库操作失败还是业务逻辑抛异常。我现在的习惯是压测不过不上线回调重放不过不发版这两条做到位商城项目交付出去能少收到一半深夜求助消息。希望帮到你。本文还有配套的精品资源点击获取
返回列表