资讯动态

网络商品销售管理系统源码拆解:商品维护到订单闭环

发布时间:2026/10/17 7:55:32 来源:尧图企业网站定制
简介这是一套面向电子商务开发者与计算机专业学生的网络商品销售管理系统源码基于C#与ASP.NET技术栈构建采用分层架构设计适合用于课程设计、毕业设计或中小型电商项目的二次开发参考。压缩包共55个文件约88KB包含15个dll动态链接库、13个cs源码文件、12个pdb调试符号文件以及aspx页面、csproj项目文件、sql脚本和config配置等覆盖数据访问层、业务逻辑层与模型层的完整结构。系统围绕商品维护、库存控制、订单处理、配送管理与数据分析等模块展开商品查询与录入、图片上传优化、库存阈值提醒、多支付方式集成等设计均有对应实现。已有156人学习下载读者可借此理解分层架构的代码组织方式掌握商品与分类管理、数据库操作封装等核心逻辑并在此基础上扩展自己的电商功能模块。1. 网络商品销售管理系统从商品维护到订单闭环一份能跑通的源码拆解很多做后台管理系统的开发者都有过这种体验需求文档里写着“商品维护”四个字真动手时才发现要处理分类层级、SKU 组合、库存扣减、上下架状态机还要跟订单模块对账。这套网络商品销售管理系统源码核心解决的就是从商品录入到订单生成这条主链路的完整闭环。它适合正在做课程设计的学生、需要快速搭后台原型的独立开发者以及想参考一套完整 CRUD 业务状态流转实现的后端新手。我拿到包之后先跑了一遍主流程又翻了几个关键模块的代码下面把能复现的部分和踩过的坑都摊开讲。2. 环境搭建与数据库初始化把项目跑起来的第一公里2.1 技术栈确认与依赖安装这套系统常见的技术组合是后端 Spring Boot MyBatis-Plus前端 Vue Element UI数据库 MySQL 8.0。拿到源码包后别急着改代码先把依赖版本对齐。我一般会先看pom.xml和package.json里的版本号确认 JDK 用 1.8 还是 11Node 用 14 还是 16。版本不对齐是新手翻车最多的地方比如 MyBatis-Plus 3.4 和 3.5 在分页插件配置上就有差异。后端依赖安装# 进入后端目录清理并下载依赖 cd backend mvn clean install -DskipTests # 如果下载慢在 settings.xml 里配国内镜像源 # 注意不要用公司内网仓库地址用公共镜像即可前端依赖安装# 进入前端目录 cd frontend # 安装依赖建议用 npm ci 而不是 npm install # npm ci 严格按照 package-lock.json 安装避免版本漂移 npm ci # 启动开发服务器 npm run serve这里解释一下为什么推荐npm cinpm install会尝试更新 lock 文件里的版本多人协作时容易出现“我这里能跑你那里报错”的玄学问题。npm ci直接按锁定版本安装适合首次复现。2.2 数据库建表与初始数据导入数据库初始化是第二个容易卡住的点。源码包里一般会带一个sql目录里面通常有两个文件schema.sql建表语句和data.sql初始数据。常见做法是先建库再导表-- 创建数据库字符集用 utf8mb4 CREATE DATABASE mall_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 切换到该库 USE mall_db; -- 导入建表脚本在命令行执行 -- mysql -u root -p mall_db sql/schema.sql -- mysql -u root -p mall_db sql/data.sql导入完成后重点检查三张表product商品主表、product_category分类表、product_sku规格表。这三张表的关联字段是category_id和product_id如果初始数据里分类 ID 对不上商品列表页会显示空白。我遇到过data.sql里分类数据插入了但商品数据的外键指向了不存在的分类页面不报错但查不出数据排查了半天。提示导入前先确认 MySQL 的sql_mode是否包含STRICT_TRANS_TABLES严格模式下日期字段为0000-00-00会直接报错而有些旧脚本里确实写了这种默认值。2.3 配置文件修改与启动验证后端配置文件通常是application.yml或application-dev.yml需要改的地方就三处数据库连接、端口、文件上传路径。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/mall_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver # 文件上传目录商品图片会存到这里 file: upload-path: /data/upload/serverTimezone必须配不配的话 MySQL 8 驱动会报时区错误。upload-path指向的目录要提前建好并给写权限否则上传商品图时会抛FileNotFoundException但前端可能只提示“上传失败”看不到具体原因。启动顺序先起后端mvn spring-boot:run看到Started Application in x seconds再起前端。前端vue.config.js里一般配了代理把/api转发到localhost:8080如果后端没起就访问前端接口全部 502。3. 商品维护模块拆解分类、SKU 与上下架状态机3.1 商品分类的树形结构与递归查询商品分类是典型的树形结构表里用parent_id自关联。查全量分类树时常见做法有两种递归查数据库或者一次查出所有节点在内存里组装。这套源码用的是后者因为分类数据量小一次查完更省连接。// 查询所有分类在内存中组装成树 public ListCategoryVO listCategoryTree() { // 一次查出所有分类避免递归查库 ListCategory all categoryMapper.selectList(null); // 按 parentId 分组 MapLong, ListCategoryVO childrenMap all.stream() .map(this::convertToVO) .collect(Collectors.groupingBy(CategoryVO::getParentId)); // 从根节点开始递归挂载子节点 ListCategoryVO roots childrenMap.getOrDefault(0L, new ArrayList()); roots.forEach(root - buildChildren(root, childrenMap)); return roots; } private void buildChildren(CategoryVO node, MapLong, ListCategoryVO childrenMap) { ListCategoryVO children childrenMap.getOrDefault(node.getId(), new ArrayList()); node.setChildren(children); // 递归处理每一层 children.forEach(child - buildChildren(child, childrenMap)); }参数说明parentId为 0 表示根节点这是约定俗成的写法。如果初始数据里根节点的parent_id写成了 null 而不是 0getOrDefault(0L, ...)会返回空列表整棵树就没了。这是我在一个模拟项目里真实踩过的坑查了半天 SQL 没问题最后发现是数据里根节点 parent_id 为 null。3.2 SKU 规格组合的生成与库存维护商品有多个规格维度时比如颜色和尺寸SKU 是这些维度的笛卡尔积。前端传过来的是规格值数组后端要生成所有组合。常见做法是用递归或迭代做笛卡尔积// 根据规格值生成 SKU 组合 public ListSku generateSkus(ListListString specValues) { ListListString result new ArrayList(); // 从第一个规格开始逐步做笛卡尔积 result.add(new ArrayList()); for (ListString values : specValues) { ListListString temp new ArrayList(); for (ListString existing : result) { for (String value : values) { ListString newCombination new ArrayList(existing); newCombination.add(value); temp.add(newCombination); } } result temp; } // 转成 Sku 对象库存默认给 0 return result.stream().map(combo - { Sku sku new Sku(); sku.setSpecJson(JSON.toJSONString(combo)); sku.setStock(0); return sku; }).collect(Collectors.toList()); }逻辑说明假设颜色有 2 个值、尺寸有 3 个值最终生成 6 个 SKU。specJson字段存的是规格组合的 JSON 字符串方便前端回显。库存默认给 0 而不是随便给个数字是为了强制运营人员手动填避免超卖。这里有个边界如果某个规格维度为空数组笛卡尔积结果会是空列表前端要加校验拦截。3.3 商品上下架的状态流转与库存扣减时机商品状态一般有草稿、待审核、已上架、已下架。状态流转不是随便改的要有前置校验。比如已上架的商品不能直接删除必须先下架。这套源码里用了一个简单的状态机// 商品状态枚举 public enum ProductStatus { DRAFT(0, 草稿), PENDING(1, 待审核), ON_SALE(2, 已上架), OFF_SALE(3, 已下架); private final int code; private final String desc; // 构造方法和 getter 省略 } // 上架操作只有待审核或已下架状态才能上架 public void onSale(Long productId) { Product product productMapper.selectById(productId); if (product null) { throw new BizException(商品不存在); } // 状态校验 if (product.getStatus() ! ProductStatus.PENDING.getCode() product.getStatus() ! ProductStatus.OFF_SALE.getCode()) { throw new BizException(当前状态不允许上架); } // 校验库存所有 SKU 库存不能全为 0 Long totalStock skuMapper.sumStockByProductId(productId); if (totalStock null || totalStock 0) { throw new BizException(库存为 0无法上架); } product.setStatus(ProductStatus.ON_SALE.getCode()); productMapper.updateById(product); }库存扣减的时机是下单时而不是加购时这点很关键。加购只锁库存不扣减下单才真正扣。如果加购就扣库存用户放弃支付后库存要回滚逻辑复杂度翻倍。这套源码用的是下单扣减 超时回滚的方案订单 30 分钟未支付自动取消并恢复库存。注意库存扣减要用update product_sku set stock stock - #{num} where id #{id} and stock #{num}这种带条件的更新靠数据库行锁保证不超卖。先查再改在并发下一定出问题。4. 订单模块与商品模块的联动下单、拆单与对账4.1 下单主流程与商品快照订单模块和商品模块的联动点在于下单时要生成商品快照。为什么不能直接关联商品表因为商品信息会变价格、名称改了之后历史订单显示的就是新数据对不上账。快照表order_item里冗余存商品名称、价格、规格、图片。// 创建订单项写入商品快照 public OrderItem createOrderItem(Sku sku, Integer quantity) { Product product productMapper.selectById(sku.getProductId()); OrderItem item new OrderItem(); item.setOrderId(null); // 后续回填 item.setProductId(product.getId()); item.setSkuId(sku.getId()); // 快照字段下单那一刻的名称和价格 item.setProductName(product.getName()); item.setProductImage(product.getMainImage()); item.setSpecJson(sku.getSpecJson()); item.setPrice(sku.getPrice()); item.setQuantity(quantity); // 小计金额 item.setTotalPrice(sku.getPrice().multiply(new BigDecimal(quantity))); return item; }参数说明price用BigDecimal而不是double金额计算不能用浮点数这是血泪经验。specJson直接从 SKU 复制保证订单里显示的规格和下单时一致。4.2 订单拆单逻辑与多商品处理一个订单包含多个商品时如果这些商品属于不同店铺或不同仓库需要拆单。这套源码是单店铺模型拆单逻辑简化了但保留了按商品维度拆分的接口。常见做法是购物车结算时按店铺分组每个店铺生成一个子订单。// 按店铺分组拆单 public ListOrder splitOrder(ListCartItem cartItems, Long userId) { // 按 shopId 分组 MapLong, ListCartItem grouped cartItems.stream() .collect(Collectors.groupingBy(CartItem::getShopId)); ListOrder orders new ArrayList(); for (Map.EntryLong, ListCartItem entry : grouped.entrySet()) { Order order new Order(); order.setUserId(userId); order.setShopId(entry.getKey()); order.setOrderNo(generateOrderNo()); // 计算总金额 BigDecimal total entry.getValue().stream() .map(CartItem::getTotalPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); order.setTotalAmount(total); orders.add(order); } return orders; }逻辑说明generateOrderNo()一般用时间戳 用户 ID 后四位 随机数保证唯一且可读。拆单后每个子订单独立支付、独立发货但对外展示时可以聚合。如果项目不需要拆单把shopId固定成同一个值就行。4.3 库存回滚与订单取消的补偿机制订单超时未支付要取消并回滚库存。这里有个坑回滚库存时如果商品已经下架或删除SKU 可能查不到。所以回滚逻辑要容错查不到 SKU 就跳过记录日志人工处理。// 取消订单并回滚库存 Transactional(rollbackFor Exception.class) public void cancelOrder(Long orderId) { Order order orderMapper.selectById(orderId); if (order null || order.getStatus() ! OrderStatus.UNPAID.getCode()) { return; } // 更新订单状态为已取消 order.setStatus(OrderStatus.CANCELLED.getCode()); orderMapper.updateById(order); // 回滚每个订单项的库存 ListOrderItem items orderItemMapper.selectByOrderId(orderId); for (OrderItem item : items) { Sku sku skuMapper.selectById(item.getSkuId()); if (sku null) { // SKU 已被删除记录日志不阻断流程 log.warn(回滚库存失败SKU 不存在: {}, item.getSkuId()); continue; } // 加回库存 skuMapper.increaseStock(item.getSkuId(), item.getQuantity()); } }Transactional保证订单状态和库存回滚在同一个事务里要么都成功要么都失败。increaseStock对应的 SQL 是update product_sku set stock stock #{num} where id #{id}直接加不校验上限因为回滚的是之前扣掉的量。5. 避坑与排查商品维护和订单联动中的五个真实问题5.1 商品列表查不出数据但数据库有记录现象前端商品管理页显示空白分页组件显示总数为 0但直接查数据库product表有数据。原因MyBatis-Plus 的逻辑删除字段配置问题。表里通常有deleted字段实体类上加了TableLogic如果初始数据里deleted默认值是 1 而不是 0所有数据都被当成已删除过滤掉了。解决检查data.sql里deleted字段的默认值统一改成 0。或者临时把实体类上的TableLogic注掉验证一下确认是这个问题再改数据。5.2 SKU 库存扣成负数现象并发下单后某个 SKU 的库存变成负数超卖了。原因扣减库存的 SQL 写成了update product_sku set stock stock - #{num} where id #{id}没有加and stock #{num}条件。并发时两个线程同时查到库存为 1都执行扣减结果变成 -1。解决SQL 必须带条件更新靠数据库行锁保证原子性。更新后判断影响行数如果为 0 说明库存不足抛异常回滚事务。-- 正确的扣减写法 UPDATE product_sku SET stock stock - #{num} WHERE id #{id} AND stock #{num};5.3 商品图片上传后访问 404现象上传商品主图成功数据库里也存了路径但前端img标签加载图片时 404。原因上传路径是服务器本地磁盘路径比如/data/upload/xxx.jpg但前端访问的是http://localhost:8080/upload/xxx.jpg后端没有配静态资源映射。解决在后端加一个配置类把上传目录映射到 URL 路径。Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到本地磁盘目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }5.4 订单金额计算出现 0.01 元误差现象购物车多个商品总价和订单实付金额差几分钱。原因金额字段用了double或float浮点数累加有精度损失。解决所有金额字段用BigDecimal数据库用decimal(10,2)。Java 里BigDecimal的加减乘除要用对应方法不要用-*/。5.5 商品下架后购物车仍能结算现象商品已下架但用户购物车里还有结算时居然能生成订单。原因下单接口没有校验商品状态只校验了库存。解决创建订单项时加状态校验商品状态不是“已上架”就抛异常提示用户该商品已下架。if (product.getStatus() ! ProductStatus.ON_SALE.getCode()) { throw new BizException(商品 [ product.getName() ] 已下架); }6. 进阶技巧用定时任务做库存预警与订单超时处理跑通主流程之后有两个进阶功能值得加上库存预警和订单超时自动取消。这两个都用定时任务实现Spring Boot 里加EnableScheduling和Scheduled就行。库存预警的逻辑是每天凌晨扫一遍 SKU 表把库存低于阈值的商品查出来生成预警记录或发通知。阈值可以配在application.yml里比如默认 10。Component public class StockWarnTask { Value(${stock.warn-threshold:10}) private Integer warnThreshold; // 每天凌晨 2 点执行 Scheduled(cron 0 0 2 * * ?) public void checkStock() { // 查库存低于阈值的 SKU ListSku lowStockSkus skuMapper.selectLowStock(warnThreshold); if (lowStockSkus.isEmpty()) { return; } // 按商品分组生成预警信息 MapLong, ListSku grouped lowStockSkus.stream() .collect(Collectors.groupingBy(Sku::getProductId)); grouped.forEach((productId, skus) - { log.warn(商品 {} 有 {} 个 SKU 库存不足, productId, skus.size()); // 实际项目里这里可以发邮件或站内信 }); } }订单超时处理是每 5 分钟扫一次未支付订单创建时间超过 30 分钟的就取消并回滚库存。这里要注意别用select *全表扫加时间范围条件走索引。Scheduled(cron 0 */5 * * * ?) public void cancelTimeoutOrders() { // 30 分钟前创建的未支付订单 LocalDateTime deadline LocalDateTime.now().minusMinutes(30); ListOrder timeoutOrders orderMapper.selectTimeoutUnpaid(deadline); for (Order order : timeoutOrders) { try { orderService.cancelOrder(order.getId()); } catch (Exception e) { // 单个订单取消失败不影响其他订单 log.error(取消超时订单失败: {}, order.getId(), e); } } }参数说明cron 0 */5 * * * ?表示每 5 分钟执行一次。deadline用LocalDateTime而不是Date避免时区问题。cancelOrder里已经有事务和库存回滚逻辑直接复用。验证定时任务是否生效我一般会临时把 cron 改成每分钟执行一次观察日志输出确认没问题再改回去。这个习惯是从一次线上事故留下的当时定时任务配了但没生效原因是启动类上忘了加EnableScheduling订单超时取消功能形同虚设库存一直被占用。从那以后我每次加定时任务都强制走一遍“改短周期 → 看日志 → 恢复配置”的流程。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑