B2C商城源码全解析:从Spring Boot订单闭环到部署避坑指南
简介当当网电商项目完整源代码面向初级Java开发者和需要课程设计/毕业设计项目的学生。代码以Java为核心覆盖用户、商品、购物车、地址等典型业务模块附带SQL数据库脚本、JSP动态页面、CSS样式和大量图片素材能清晰展示请求从前端到后端再落库的完整流程。压缩包共406个文件含219个gif、67个jpg等界面素材26个java源文件、22个jsp页面、15个css样式文件以及9个jar依赖包整体仅3.29MB结构紧凑易于检索目录中包含实体类、DAO、Action等分层设计便于按模块逐步阅读。已有1221人学习通过阅读、调试及二次开发可快速提升JavaWeb项目实践能力。1. 一套B2C商城完整开发源代码能跑通交易闭环才是真完整拿到一套号称完整的商城开发源代码很多人的第一反应是打开README照着部署结果不是数据库连不上就是前端资源404。这套以图书B2C商城业务为蓝本的完整开发源代码前后端加在一起覆盖了从用户注册、商品检索、购物车、下单支付到后台发货的完整交易闭环。它不是只有登录注册的Demo而是能把订单从待支付一路走到已完成的工程项目。适合想系统理解商城业务与技术实现的人也适合拿来做毕业设计或二次开发、不想从零造轮子的从业者。有了这套源码打底省下的不只是写代码的时间更是踩接口设计、库存扣减、状态机这些大坑的成本。2. 技术选型与项目结构先看清这套源码的骨架再动手2.1 前后端技术选型版本组合稳才是真的好这套源码用的技术组合是电商项目非常主流的一套后端Spring Boot加MyBatis-Plus权限用Spring Security配合JWTRedis穿插在购物车、验证码和登录态里。没有上微服务、没有上Spring Cloud Alibaba整个工程就是一个单体Maven项目。这对第一次跑通商城流程的人来说其实是好事——调试链路短不用在服务发现和网关那一层绕圈子。版本组合我特意提到前面来说因为这里有个血泪教训。源码里JDK用的1.8Spring Boot用的2.3MySQL用的5.7Redis用的6.x。如果你本机装的是JDK 17甚至更高直接spring-boot:run大概率会在启动阶段报出UnsupportedClassVersionError。不是源码有问题是版本跨度太大。这类完整项目源码往往不会追最新版本反而是当时的稳定组合所以先老老实实把JDK 8准备好能省掉第一轮翻车。提示如果电脑上同时有多个JDK版本建议在当前shell里临时export JAVA_HOME指向JDK 8而不是全局卸载高版本这样不耽误你平时用新版写其他代码。前端的话商城H5端用的Vue全家桶配上Vant组件库后台管理端用的Vue加Element UI。两个前端工程独立维护共用一套后端REST接口。接口风格统一走/api/v1前缀从Controller到Service的命名也很规整基本是见名知意不需要靠注释找逻辑。2.2 项目目录结构从根目录定位到一行业务代码把源码包解压后第一眼看上去就是一个多模块工程。我拿到的这份目录结构大致如下shangcheng/ ├── sql/ │ └── shangcheng_all.sql # 全量脚本建库、建表、基础数据 ├── backend/ │ ├── pom.xml │ └── src/main/ │ ├── java/com/example/shangcheng/ │ │ ├── controller/ # REST接口入口 │ │ ├── service/ # 业务核心下单、库存、支付逻辑都在这层 │ │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ │ ├── entity/ # 表实体映射 │ │ └── config/ # Redis、拦截器、跨域等配置 │ └── resources/ │ ├── application.yml │ ├── application-dev.yml │ └── mapper/ # 手写SQL的XML文件 ├── frontend/ │ ├── mall-app/ # 商城H5前端 │ └── mall-admin/ # 后台管理前端 └── doc/ └── deploy.md这个目录结构最大的价值在于分层做得干净。想看下单代码直接拐进OrderServiceImpl想找购物车入口沿着CartController往下翻就行。不用靠IDE的全项目搜索猜来猜去。很多电商源码喜欢把Controller、Service、Mapper揉在一起二次开发从找文件这一步就开始折腾这份就没有那个毛病。2.3 数据库脚本48张表里主链路只有11张sql目录下有一个shangcheng_all.sql导入后能建出48张表。脚本里带着大量初始化数据比如商品SPU、SKU、品牌、分类、轮播图导完以后商城首页直接就有内容不用空跑一套空壳。这点在二次开发时很关键——你把项目跑起来看到的首页是图文并茂的而不是一张白纸排查问题的时候能少分心。48张表看着多但支撑核心交易的主链路其实只有11张列成表方便对照表名作用关键字段user用户表id, mobile, password, statususer_address收货地址user_id, receiver, phone, detailcategory商品分类树id, parent_id, name, levelspu商品主表id, category_id, title, statussku库存价格表id, spu_id, price, stock, speccart_item购物车条目user_id, sku_id, quantityorders订单主表id, order_no, user_id, total, statusorder_item订单明细order_id, sku_id, quantity, pricepayment_log支付流水order_id, pay_type, amount, statusafter_sale售后申请order_id, reason, statuscoupon优惠券user_id, type, amount, expire_time建议动手前先把表结构过一遍画出user到order_item这条线。我见过不少人拿到源码就跑结果订单流程只走到一半就中断翻数据库半天才明白是order_item的外键字段名对不上。这种基础工作做在前面后面查问题就是按图索骥。2.4 本地启动改对两处配置十分钟跑通先导入数据库脚本mysql -uroot -p sql/shangcheng_all.sql注意如果MySQL是5.7以上且设置了密码这行命令里请把-p后面的密码补齐或者等提示后手输。导入过程大概几十秒脚本较大不要中途强制关终端。然后改后端配置。文件在backend/src/main/resources/application-dev.ymlspring: datasource: url: jdbc:mysql://localhost:3306/shangcheng?useUnicodetruecharacterEncodingutf8useSSLfalse username: root password: 你的密码 redis: host: localhost port: 6379URL里的characterEncodingutf8是第一个坑位删掉之后数据库写入中文会变问号。password字段如果本地MySQL没设密码就留空字符串设置过密码就写正确密码写错的话启动日志会不停刷Access denied for user那就不只是配置问题了。后端启动cd backend mvn spring-boot:run看到Started Application字样后后端就算起来了。接着另开一个终端启动前端cd frontend/mall-app npm install npm run servenpm install时间取决于网络国内的话建议先配好镜像源再装。前端默认开发端口是3000后端接口地址写在src/utils/request.js里默认指向8080端口匹配就不用改。跑起来之后浏览器访问本地3000端口能看到带数据的商城首页整个本地环境就算拉通了。3. 核心业务模块实现从JWT登录到订单状态机3.1 用户认证JWT加Redis双保险的登录设计用户模块是整套源码里的第一个完整链路也是权限体系的起点。user表里密码字段存的是加盐后的MD5值不是明文。虽然现在更推荐BCrypt但在教学级项目里MD5加盐已经是及格线二次开发时想换BCrypt也是局部的改动不涉及全链路。核心的登录逻辑写在UserServiceImpl里精简后是这样的Override public String login(String mobile, String password) { // 1. 按手机号先查出用户 User user this.getOne(new QueryWrapperUser().eq(mobile, mobile)); if (user null) { throw new BizException(该手机号尚未注册); } // 2. 加盐MD5比对 String md5 MD5Util.md5(password user.getSalt()); if (!md5.equals(user.getPassword())) { throw new BizException(密码错误); } // 3. 签发JWT并写入Redis24小时有效 String token JwtUtil.createToken(user.getId(), user.getNickname()); redisTemplate.opsForValue().set( login:token: token, user.getId().toString(), 24, TimeUnit.HOURS); return token; }第3步的Redis写入可能是整个源码里优先级最高的设计。JWT本身无状态token一旦发出去在有效期内谁也拦不住它。但后端在Redis里额外以login:token:xxx为key存了一份这样就可以在用户退出时主动删掉这个key实现真正意义上的强制下线。如果只做JWT不落Redis退出登录就是个摆设功能。想调整登录有效期改set方法里的24和TimeUnit.HOURS就行例如改成7天就是7, TimeUnit.DAYS。3.2 商品模块SPU/SKU分离和LIKE查询的取舍商品模块的结构是电商领域最经典的SPU加SKU两层模型。SPU存商品的公共属性比如书名、封面、简介SKU存具体规格的价格和库存比如精装版的价格和库存就是单独一个SKU。前端切换规格时实际上是在切换SKU加入购物车的也是SKU的ID。分类表category用parent_id实现树形结构H5端首页的分类侧边栏就是递归查出来的。值得注意的是商品搜索这里没有引入Elasticsearch直接用的MySQL的LIKE查询public PageSpuVO search(String keyword, Integer categoryId, Integer pageNo, Integer pageSize) { QueryWrapperSpu wrapper new QueryWrapper(); // 1. 按标题或副标题模糊匹配 if (StringUtils.isNotBlank(keyword)) { wrapper.and(w - w.like(title, keyword) .or().like(subtitle, keyword)); } // 2. 按分类过滤 if (categoryId ! null) { wrapper.eq(category_id, categoryId); } // 3. 只出上架商品 wrapper.eq(status, 1); // 4. 按销量优先、创建时间倒序 wrapper.orderByDesc(sale_count).orderByDesc(create_time); return spuMapper.selectPage(new Page(pageNo, pageSize), wrapper); }LIKE查询的代价是数据量大了以后索引会失效尤其在keyword以%开头的情况下。但在这套源码的典型数据量下性能完全够用。如果以后要换ES思路也清楚商品上下架时同步一份到ES搜索接口换成ES客户端查询Controller层调用不变。这个预留的改造点是很多源码没有的。3.3 购物车Redis的Hash结构把未登录也兜住购物车这个模块藏着不少电商心得。H5端未登录时购物车数据存在浏览器的localStorage里登录后前端会调用购物车合并接口把本地的条目批量传给后端后端统一存进Redis。存储结构用的是Hashkey是cart:userIdfield是skuIdvalue是数量。这样每次加购只需要一次HSET不会像List那样需要先查后改public void addToCart(Long userId, Long skuId, Integer count) { String key cart: userId; // 1. 校验SKU存在且可售 Sku sku skuMapper.selectById(skuId); if (sku null || sku.getStatus() ! 1) { throw new BizException(商品已下架); } // 2. 取出当前数量并与新数量累加 Object val redisTemplate.opsForHash().get(key, skuId.toString()); int oldCount val null ? 0 : Integer.parseInt(val.toString()); int newCount oldCount count; // 3. 库存软校验 if (newCount sku.getStock()) { throw new BizException(库存不足当前剩余 sku.getStock()); } redisTemplate.opsForHash().put(key, skuId.toString(), newCount); }加购时的库存校验是软校验意义在于给用户即时反馈。真正卡死库存的是下单时的硬校验因为购物车可能在前一天加过第二天库存已经变了。分层校验这个思路在面试里是加分项在实现里也确实是这套源码处理高并发下单的基础。3.4 订单模块乐观锁扣库存到支付状态流转订单模块是整个项目的精髓。下单接口在OrderController的/order/submit路径整个流程做了四件事生成订单号、扣库存、落订单和明细、清购物车。其中最值得展开的是库存扣减它用的是乐观锁public boolean deductStock(Long skuId, Integer count) { // UPDATE ... WHERE id #{skuId} AND stock #{count} return skuMapper.deductStock(skuId, count) 0; }对应的SQL在resources/mapper的SkuMapper.xml里update iddeductStock UPDATE sku SET stock stock - #{count}, version version 1 WHERE id #{skuId} AND stock #{count} /updateWHERE里的stock #{count}是防超卖的核心。两个请求同时扣减同一SKU库存时数据库行锁会保证第二个UPDATE的受影响行数为0代码拿到false就抛库存不足不会出现卖超。相比先select加锁再update的写法这种乐观锁在并发上的表现好得多也是很多电商项目的标准姿势。支付部分用的是模拟支付回调。前端点支付后跳到模拟支付页面支付接口/payment/mock先检查订单是待支付状态再把它推进到已支付同步写支付流水。真实微信或支付宝支付被统一封装在PayService接口里目前实现类只有MockPayServiceImpl接真实支付时替换实现类就可以业务层的状态机不用动。4. 部署到服务器环境准备、脚本化构建和Nginx配置4.1 服务器环境准备JDK 8和Maven的安装顺序前面再三强调JDK 8到真正部署时这个要求更重要。生产服务器的操作系统我以CentOS 7为例新系统版本命令会有出入但思路一样。安装JDK 8yum install -y java-1.8.0-openjdk echo export JAVA_HOME/usr/lib/jvm/java-1.8.0-openjdk /etc/profile echo export PATH\$JAVA_HOME/bin:\$PATH /etc/profile source /etc/profile java -version看到openjdk version 1.8.0_xxx的输出才算装好。注意PATH那行我特意加了反斜杠转义避免$JAVA_HOME在写入profile时被shell立即展开成空值。这是一个非常容易忽略的小坑写脚本时如果用双引号$符号不转义就会失效。Maven如果只是为了跑现成jar包可以不装但想改代码再打包就得装。装好后在backend目录下执行mvn -v能打印版本说明环境变量没问题。4.2 后端构建跳过测试避开一个必踩的坑后端打包命令是cd backend mvn clean package -DskipTests-DskipTests这个参数的意义我在第一次打包时没有加结果构建在test阶段直接失败——源码里的测试类依赖本地的MySQL和Redis环境在服务器上跑这些测试大概率起不来。加上DskipTests后跳过测试直接出jar包一次就能过。打包产物在backend/target目录下名字一般是shangcheng-server.jar体积在100MB上下因为内嵌了Tomcat和全部依赖。放到服务器的/app目录下配合一个简单的启动脚本管理#!/bin/bash # /app/deploy-backend.sh APP_NAMEshangcheng-server JAR/app/$APP_NAME.jar nohup java -jar $JAR \ --spring.profiles.activeprod \ --server.port8080 \ /app/logs/backend.log 21 echo Backend started, PID: $!这里的--spring.profiles.activeprod让后端走生产环境配置对应application-prod.yml里的独立数据源和Redis地址。首次启动后马上看一眼/app/logs/backend.log确认没有Unable to connect to Redis或Communications link failure之类的报错再继续往下走前端。4.3 前端发布与Nginx把H5商城挂上域名前端构建cd frontend/mall-app npm install npm run build构建前必须确认一件事前端生产环境请求接口的地址写在哪里。这套源码在.env.production文件里配置默认是相对路径这样方便Nginx做代理转发。如果当时改成了http://localhost:8080就一定要改成服务器的公网地址或你的域名否则线上页面所有接口全部打向本机。构建产物在dist目录把它传到服务器的/app/frontend/dist下然后配置Nginxserver { listen 80; server_name your-domain.com; root /app/frontend/dist; index index.html; # 前端路由用History模式必须配这个回退 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存7天 location ~* \.(js|css|png|jpg|jpeg|gif|svg)$ { expires 7d; add_header Cache-Control public, max-age604800; } }root与location里的匹配是基础真正容易错的是proxy_pass带不带后缀这个问题在下一章会专门展开。改完配置后nginx -t nginx -s reload两行命令都能顺利执行访问域名首页正常显示、登录接口返回正常部署就算成功。如果页面出来了但接口全是404优先去查proxy_pass写的是不是带上了/api/这个前缀。4.4 上线前检查四个必须改的默认配置部署能跑通了不代表可以直接放出去。以下四处默认配置上线前不改有极大的裸奔风险。第一处是数据库账号。生产环境不要用root连库单独建一个账号只授权给shangcheng这一库。第二处是Redis。要么给Redis设密码要么把bind从0.0.0.0改成内网或回环地址不能让公网直接访问。第三处是JVM参数。2核4G的服务器我一般加-Xms512m -Xmx1024m不然默认堆容量按物理内存比例分配可能把机器内存吃满。第四处是管理后台的默认管理员密码初始化数据里是admin123不改的话任何能访问后台域名的人都能以管理员身份登录。5. 避坑指南这套商城源码最常见的五个问题这一章是实打实的血泪经验。以下五个问题全部是我在这套源码上亲手踩过、再一步步查出来解决的不是网上抄来的二手总结。按现象、原因、解决的顺序写你可以直接对照自己的情况排查。5.1 登录后接口全部401跨域与拦截器的顺序冲突现象前端能打开登录接口正常返回token但登录后访问商品列表、购物车这些接口全部返回401。原因WebConfig里同时注册了拦截器和CORS跨域配置但拦截器的preHandle先执行且它在判token之前先检查了Origin头。跨域请求的预检OPTIONS请求没有带token于是被拦截器直接挡了压根没走到Controller。解决在拦截器的preHandle里对OPTIONS请求直接放行再加一个判断放行登录和商品公开接口。改动量就几行但效果立竿见影。改完后清一下浏览器缓存重新登录接口状态立刻从401变成200。5.2 Nginx代理后接口404proxy_pass的路径拼接规则现象后端服务本地curl 127.0.0.1:8080/api/product/list能正常返回JSON但通过Nginx域名访问产品列表时全部404。原因Nginx的proxy_pass在带或不带URI后缀时行为完全不同。写成proxy_pass http://127.0.0.1:8080;的时候Nginx会把原始请求URI原样转发后端收到/api/product/list能正确命中Controller。如果写成了proxy_pass http://127.0.0.1:8080/;Nginx会把匹配到的/api/前缀剥掉再转发后端收到的是/product/list匹配不上接口路由。解决记住一条规则——希望保留/api前缀用proxy_pass http://127.0.0.1:8080/api/;这种写法。后端Controller的RequestMapping是/api/**两边对齐了404就不会出现。这个坑前后端联调时最容易遇到遇到先看代理规则别急着怀疑后端没起来。5.3 改了环境变量不生效构建缓存要一起清现象改完.env.production里的接口地址重新npm run build再部署页面请求的依然是旧地址。原因前端构建工具会把环境变量解析结果缓存到node_modules/.cache里。某些版本下即使.env.production改了构建工具读到的还是缓存里的旧值。看起来就是我改了怎么不变的玄学现场。解决每次构建前先执行rm -rf node_modules/.cache dist再重新npm run build。这个习惯我后来保留到了所有前端项目里不管是不是这套源码。另外检查页面实际加载的JS文件名如果发现文件名还是旧哈希基本就是缓存没清干净。5.4 支付回调重复执行状态变更必须带条件现象模拟支付接口被重复调用了两次订单发了两遍货状态直接跳到已完成复现时才发现多扣了库存。原因支付回调没有做幂等处理。两个请求几乎同时到达都查询到订单状态是待支付于是各自执行了更新。第一个请求的事务还没提交第二个查询到的还是旧状态第二次更新照样成功。解决状态更新SQL必须带上旧状态条件例如UPDATE orders SET status 1 WHERE id #{id} AND status 0这样第二个请求的受影响行数会是0配合Service层在更新前再判一次状态双重保险。代码里要改的是OrderService处理支付回调的方法改动不大但这是生产级别的必修课。5.5 中文乱码三件套数据库、连接串、响应头缺一不可现象商品标题、用户昵称、收货地址这些字段写入数据库后变成问号后端日志里看却是正常的中文。原因数据库默认字符集是latin1JDBC连接串没带characterEncodingutf8接口响应的Content-Type里缺charset。只要这三处有一处不对中文就会在链路某一段变成乱码。解决创建数据库时就指定字符集CREATE DATABASE shangcheng DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;然后检查application-prod.yml里的URL必须带useUnicodetruecharacterEncodingutf8。最后在后端配置里把HttpMessageConverter默认编码改成UTF-8。三个层面都对齐中文字段才算真正安全。我后来每接手一个项目先跑一遍字符集三件套不等到线上出乱码再查。6. 二次开发把文件上传从本地改成云存储这套源码里文件上传默认落的是本地磁盘逻辑在FileServiceImpl的upload方法里把MultipartFile写到项目目录下的uploads文件夹然后拼接一个相对URL返回。本地开发完全没问题但一放到线上多实例部署就露馅——上传目录是单机的图片从这里上传的请求和从那里读取静态资源的请求很可能命中两台不同的机器结果就是页面图片大面积404。另一个更基础的问题是容灾与备份。本地磁盘一旦故障所有用户头像和商品图全没。所以线上运营的商城项目文件存储一般都会迁到对象存储服务上。改造的核心其实就是替换upload方法我用兼容对象存储协议的客户端为例public String upload(MultipartFile file) { // 1. 限制文件类型防止上传恶意文件 String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); if (!Arrays.asList(.jpg, .png, .gif, .webp).contains(suffix.toLowerCase())) { throw new BizException(图片格式不支持); } // 2. 用时间戳加随机串生成唯一对象名 String objectKey upload/ System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ) suffix; // 3. 调用对象存储SDK上传 cloudStorageClient.putObject(bucketName, objectKey, file.getInputStream()); // 4. 返回完整的公网访问URL return https://your-bucket.storage.example.com/ objectKey; }第2步的唯一文件名是关键。本地版本有个老毛病两个用户同时上传同名的图片后后者覆盖前者。改成时间戳加UUID后重复概率可以忽略不计。第3步是接入对象存储SDK的核心调用bucketName和密钥要从配置中心读取不能硬编码。改造完之后还需要处理历史数据数据库里旧图片很多是/uploads/开头的相对路径不重写成完整URL的话老商品图片依然打不开。我的习惯是在改造当天写一个一次性迁移脚本把所有历史图片字段批量替换为新域名前缀跑完再验一下商品详情页确认没有漏网的。从那以后我每次拿到这类商城源码第一件事就是搜一遍upload上传入口和所有静态资源代理配置确认存储方案能撑住线上流量再开始动业务代码。顺序反过来后面都是补窟窿的时间。希望这些经验能帮到你。本文还有配套的精品资源点击获取