轻量级电商后台:SPU/SKU分离+商品状态机+SKU矩阵生成
简介这是一套基于ASP.NET开发的网络商品销售管理系统源码面向初学者与Web开发学习者聚焦商品维护、订单处理与库存管理等核心电商功能实现。资源共55个文件包含15个DLL动态库封装业务逻辑、13个C#源文件如Product.cs、ProductManager.cs等分层实现、12个PDB调试文件以及ASPx页面、SQL脚本、项目解决方案.sln和配置文件等完整呈现三层架构DAL/BLL/Models设计包体仅88KB轻量易读。已有157人学习下载适合通过源码理解典型Web系统模块划分、数据库交互含SQLQuery1.sql及前后端协同流程。读者可直接导入Visual Studio运行调试掌握商品增删改查、分类管理、默认页跳转逻辑等实战要点并参考清晰的目录结构快速定位各层职责。1. 网络商品销售管理系统一个能当天部署、次日上线的轻量级电商后台专治小团队“改个价格要改三处、上架新品靠Excel传”的血泪现场你有没有遇到过这种场景运营在群里发消息“这个SKU的库存错了赶紧调成87”后端查数据库发现字段叫stock_num前端页面显示的是available_stock而导出报表用的又是realtime_inventory——三个值居然不一致或者市场临时加推一个限时套装技术说“得改接口、发版本、走测试流程”结果活动开始前两小时还在联调。这个「网络商品销售管理系统」就是为这类真实翻车现场设计的它不追求大而全的SaaS功能而是聚焦「商品维护」这一核心链路把类目管理、SPU/SKU建模、上下架状态机、库存快照、价格版本控制这五件事做深做稳。前后端分离架构Vue3 Spring Boot 2.7 MyBatis-PlusMySQL 5.7Docker一键打包。某高校实验室用它支撑了3个学生创业项目的线上商城后台从下载到跑通首页商品列表编辑页平均耗时47分钟。如果你正被“改个商品属性要动三套代码”折磨或想快速搭一个可交付、可演示、可二次开发的电商基座这个项目不是玩具是能立刻拧上螺丝的工具箱。2. 技术栈与工程结构Spring Boot 2.7 Vue3 的最小可行组合目录即文档这个系统刻意规避了微服务、Redis集群、ES搜索等重型组件所有能力都收敛在单体应用内但结构清晰、分层明确。后端采用标准的三层架构Controller → Service → Mapper前端按功能域组织模块没有过度抽象。整个工程就是两个独立目录backend/和frontend/没有隐藏的构建脚本或私有NPM源——所有依赖均来自Maven Central和npmjs.org。2.1 后端模块拆解从Controller到Mapper的完整映射链backend/src/main/java/com/example/shop/下的核心包结构如下├── controller/ │ ├── GoodsController.java // 商品CRUD主入口含分页查询、批量上下架 │ ├── CategoryController.java // 类目树形接口支持无限级递归 │ └── SkuController.java // SKU粒度操作如库存调整、价格更新 ├── service/ │ ├── impl/ │ │ ├── GoodsServiceImpl.java // 商品主逻辑SPU聚合根管理、状态流转校验 │ │ └── SkuServiceImpl.java // SKU快照生成、库存扣减原子性保障 ├── mapper/ │ ├── GoodsMapper.java // MyBatis-Plus BaseMapper扩展含自定义SQL │ └── SkuMapper.java // 复合条件查询按SPU ID 规格值组合查SKU └── entity/ ├── Goods.java // SPU实体名称、主图、详情富文本、类目ID、审核状态 └── Sku.java // SKU实体规格JSON、价格、库存、上架状态、创建时间戳关键设计点在于SPU与SKU物理分离存储。Goods表只存商品共性信息如“iPhone 15 Pro”Sku表存具体规格如“256GB 银色 移动版”通过goods_id外键关联。这样修改SPU标题或主图不会影响已售出SKU的历史快照也避免了传统单表存储导致的冗余字段爆炸。2.2 前端模块组织Vue3 Composition API Pinia状态管理拒绝魔改frontend/src/目录结构直白到像教科书├── views/ │ ├── goods/ │ │ ├── GoodsList.vue // 商品列表页表格搜索批量操作栏 │ │ ├── GoodsEdit.vue // 商品编辑页Tab切换基本信息/规格参数/详情描述 │ │ └── SkuTable.vue // 内嵌子组件动态渲染SKU规格矩阵颜色×内存×版本 ├── stores/ │ ├── useGoodsStore.js // Pinia store封装商品列表分页、缓存当前编辑SPU │ └── useCategoryStore.js // 类目树缓存支持懒加载子节点 ├── api/ │ ├── goods.js // Axios封装/api/goods/list, /api/goods/save, /api/sku/update-stock │ └── category.js // /api/category/tree, /api/category/create └── utils/ └── sku-matrix.js // 核心算法将规格选项颜色:[黑,白],内存:[128,256]生成笛卡尔积SKU数组SkuTable.vue是最值得细看的组件。它不依赖任何第三方表格库用纯Vue3响应式实现左侧固定规格选项列颜色、内存顶部固定SKU属性行价格、库存、条码中间单元格绑定skuList数组每个单元格是一个el-input输入即触发updateSkuField(skuId, field, value)。背后是sku-matrix.js的实时计算——当用户勾选“黑色”“256GB”自动合并生成唯一SKU ID并同步更新对应行的价格与库存字段。这种设计让运营人员无需理解JSON Schema直接在表格里填数字就能完成SKU配置。2.3 数据库设计精要三张核心表撑起全部业务系统仅依赖三张主表不含日志、权限等通用表ER关系极简-- 商品主表SPU CREATE TABLE goods ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 商品名称, category_id bigint NOT NULL COMMENT 所属类目ID, cover_image varchar(255) COMMENT 主图URL, detail_content longtext COMMENT 富文本详情, status tinyint NOT NULL DEFAULT 0 COMMENT 0-草稿,1-待审核,2-已上架,3-已下架, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ); -- SKU表具体规格 CREATE TABLE sku ( id bigint NOT NULL AUTO_INCREMENT, goods_id bigint NOT NULL COMMENT 关联SPU, specifications json NOT NULL COMMENT 规格JSON: {color:黑色,memory:256GB}, price decimal(10,2) NOT NULL COMMENT 销售价格, stock int NOT NULL DEFAULT 0 COMMENT 可用库存, sku_code varchar(50) UNIQUE COMMENT 外部条码, status tinyint NOT NULL DEFAULT 1 COMMENT 1-上架,0-下架, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_goods (goods_id) ); -- 类目表支持无限级 CREATE TABLE category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, parent_id bigint DEFAULT 0 COMMENT 父类目ID0为根, sort_order int NOT NULL DEFAULT 0 COMMENT 排序权重, PRIMARY KEY (id), KEY idx_parent (parent_id) );提示specifications字段使用MySQL原生JSON类型而非字符串。这带来两个实际好处一是可直接在SQL中用JSON_CONTAINS(specifications, 黑色, $.color)做精确查询二是MyBatis-Plus能自动序列化/反序列化Java Map对象省去手动解析JSON的胶水代码。但注意——不要在specifications上建普通索引需用生成列generated column配合函数索引才能高效查询本项目暂未启用因小规模数据下全表扫描足够快。3. 快速启动指南5分钟跑通本地环境验证商品增删改查全流程部署这个系统不需要服务器、不依赖云服务一台开发机装好JDK 11、Node.js 16、MySQL 5.7即可。整个过程无魔法命令每一步都可验证输出。3.1 后端启动从编译到接口就绪进入backend/目录执行标准Maven流程# 1. 清理并编译跳过测试以加速 mvn clean compile -Dmaven.test.skiptrue # 2. 修改数据库连接application.yml # 找到 spring: # datasource: # url: jdbc:mysql://localhost:3306/shop_db?useSSLfalseserverTimezoneAsia/Shanghai # username: root # password: your_password # 3. 初始化数据库首次运行 # 在MySQL中执行 backend/src/main/resources/sql/init.sql # 该脚本会创建shop_db库、三张主表并插入1条测试类目和2条测试商品 # 4. 启动应用 mvn spring-boot:run启动成功后控制台会输出Tomcat started on port(s): 8080 (http) Started ShopApplication in 3.2 seconds此时访问http://localhost:8080/api/goods/list?page1size10应返回JSON格式的商品列表包含total、list字段。若返回404请检查是否漏掉RestController注解或RequestMapping(/api)前缀若返回500且报Connection refused请确认MySQL服务已启动且账号密码正确。3.2 前端启动Vue Dev Server直连后端API前端默认配置为代理后端请求避免跨域cd frontend/ # 安装依赖国内用户建议用cnpm或pnpm加速 npm install # 修改代理配置vite.config.ts // 找到 server: { proxy: { /api: { ... } } } // 确保 target 指向 http://localhost:8080 # 启动开发服务器 npm run dev浏览器打开http://localhost:5173应看到登录页默认账号admin / 123456。登录后进入商品列表页点击“新增商品”填写名称、选择类目、上传图片保存后列表立即刷新——此时后端日志会打印GoodsServiceImpl.saveGoods: saved goods id1001证明SPU已写入数据库。再点击该商品右侧的“编辑规格”在SKU表格中填入价格与库存点击“保存规格”观察sku表是否新增记录。这是最关键的验证点SPU与SKU是否真正解耦查看数据库goods表新增一行sku表对应新增多行取决于规格组合数且goods_id外键指向正确。3.3 Docker一键打包三步生成可移植镜像若需部署到测试服务器用Docker可彻底屏蔽环境差异# 1. 构建后端镜像在backend/目录下 docker build -t shop-backend . # 2. 构建前端镜像在frontend/目录下 docker build -t shop-frontend . # 3. 启动容器需先运行MySQL容器 docker run -d --name shop-mysql -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 mysql:5.7 docker run -d --name shop-backend -p 8080:8080 --link shop-mysql:mysql -e SPRING_DATASOURCE_URLjdbc:mysql://mysql:3306/shop_db shop-backend docker run -d --name shop-frontend -p 80:80 --link shop-backend:backend shop-frontend注意前端镜像的Nginx配置中proxy_pass http://backend;将所有/api请求转发给后端容器。这种--link方式虽已过时但对单机测试足够稳定比写docker-compose.yml更直观。4. 避坑指南那些让我重启三次IDE才定位到的典型问题部署过程中踩过的坑比代码还真实。以下5个问题90%的新手会在前30分钟内撞上按现象→原因→解决三步法整理亲测有效。4.1 现象前端登录后空白页控制台报错Cannot find module /stores/useGoodsStore原因Vite的别名配置未生效或vite.config.ts中resolve.alias路径写错。常见错误是把/stores写成/store少s或路径指向了不存在的目录。解决检查vite.config.ts中的alias配置export default defineConfig({ resolve: { alias: { : path.resolve(__dirname, src), stores: path.resolve(__dirname, src/stores), // 必须与import路径完全一致 } } })然后在main.js中验证import { useGoodsStore } from stores/useGoodsStore—— 如果VS Code能正常跳转到文件说明别名正确否则重启Vite服务CtrlC再npm run dev。4.2 现象商品列表页加载失败Network面板显示GET http://localhost:5173/api/goods/list 404 (Not Found)原因前端代理未生效请求直接发给了Vite Dev Server5173端口而该端口无后端API。根本原因是vite.config.ts中代理配置的rewrite规则缺失或路径不匹配。解决严格按以下格式配置代理注意/api必须带前导斜杠server: { proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) // 关键去掉/api前缀再转发 } } }验证方法在浏览器直接访问http://localhost:8080/api/goods/list若能返回JSON则代理配置正确若仍404说明后端未启动或端口不对。4.3 现象保存商品时后端抛异常org.springframework.dao.DataIntegrityViolationException: Column category_id cannot be null原因前端提交的category_id为null或空字符串而数据库字段设为NOT NULL。常见于类目选择组件未绑定值或el-select的v-model未初始化。解决在GoodsEdit.vue的data()中强制初始化return { form: { name: , category_id: 0, // 必须设为0或-1不能为null cover_image: , detail_content: } }同时在后端GoodsController.save()方法中加校验PostMapping(/save) public Result save(RequestBody Goods goods) { if (goods.getCategoryId() null || goods.getCategoryId() 0) { return Result.fail(请选择商品类目); } // ... 业务逻辑 }4.4 现象SKU表格中修改价格后点击保存无反应Network无请求发出原因SkuTable.vue中updateSkuField方法未正确触发Pinia store的commit或skuList数组未用ref()包裹导致响应式失效。解决检查SkuTable.vue的setup函数const skuList ref([]); // 必须用ref包装数组 const updateSkuField (skuId, field, value) { const sku skuList.value.find(s s.id skuId); if (sku) { sku[field] value; // 直接修改属性ref会自动触发更新 } };若仍无效在clicksaveSkus事件处理器中加console.log(skuList.value)确认数组是否被修改。若log为空说明skuList未从父组件正确接收prop。4.5 现象MySQL启动后执行init.sql报错ERROR 1064 (42000): You have an error in your SQL syntax原因MySQL 5.7默认SQL模式包含STRICT_TRANS_TABLES而init.sql中某些字段定义如longtext未指定字符集或json类型在低版本不支持。解决降级兼容性在init.sql开头添加SET SQL_MODE NO_AUTO_VALUE_ON_ZERO; SET time_zone 00:00; -- 确保MySQL版本5.7.8json类型支持起点 -- 若仍报错将specifications字段改为TEXT类型并在Java中手动JSON序列化更稳妥的做法用mysqld --version确认版本若低于5.7.8直接修改建表语句将specifications json改为specifications text并在Sku.java中用TableField(typeHandler JacksonTypeHandler.class)处理序列化。5. 核心模块代码逐行解析商品状态机与SKU库存扣减的原子性保障系统最易被低估、也最常被改崩的部分是商品上下架状态流转与库存扣减。很多团队用简单UPDATE sku SET stock stock - 1 WHERE id ?结果高并发下超卖。本项目用状态机乐观锁双保险代码虽短但每行都有讲究。5.1 商品状态机用枚举策略模式杜绝非法状态跳转backend/src/main/java/com/example/shop/entity/GoodsStatus.java定义了严格的状态迁移规则public enum GoodsStatus { DRAFT(0, 草稿, Collections.emptySet()), // 初始状态 PENDING_REVIEW(1, 待审核, Set.of(DRAFT)), // 只能从草稿来 ONLINE(2, 已上架, Set.of(PENDING_REVIEW, OFFLINE)), // 能从待审或下架来 OFFLINE(3, 已下架, Set.of(ONLINE, PENDING_REVIEW)); // 能从上架或待审来 private final int code; private final String desc; private final SetGoodsStatus allowedFrom; // 允许从此状态迁入 GoodsStatus(int code, String desc, SetGoodsStatus allowedFrom) { this.code code; this.desc desc; this.allowedFrom allowedFrom; } public boolean canTransitionFrom(GoodsStatus from) { return this.allowedFrom.contains(from); } }GoodsServiceImpl.changeStatus()方法强制校验Transactional public Result changeStatus(Long goodsId, Integer targetStatus) { Goods goods goodsMapper.selectById(goodsId); GoodsStatus target GoodsStatus.fromCode(targetStatus); if (!target.canTransitionFrom(GoodsStatus.fromCode(goods.getStatus()))) { return Result.fail(String.format(状态非法从%s不能切换到%s, GoodsStatus.fromCode(goods.getStatus()).getDesc(), target.getDesc())); } // 更新状态 记录操作日志 goods.setStatus(target.getCode()); goods.setUpdatedAt(new Date()); goodsMapper.updateById(goods); logService.record(GOODS_STATUS_CHANGE, goodsId, String.format(状态变更为%s, target.getDesc())); return Result.success(); }这段代码的价值在于它把业务规则“上架只能从待审核来”固化在代码里而不是靠文档或口头约定。当产品提需求“允许草稿直接上架”你只需改allowedFrom集合无需 grep 全局找所有状态判断逻辑。5.2 SKU库存扣减乐观锁数据库行锁一次扣减不超卖库存扣减放在SkuServiceImpl.reduceStock()中核心是两条SQLUpdate(UPDATE sku SET stock stock - #{quantity}, updated_at NOW() WHERE id #{skuId} AND stock #{quantity}) int reduceStockWithCheck(Param(skuId) Long skuId, Param(quantity) Integer quantity); public Result reduceStock(Long skuId, Integer quantity) { int affected reduceStockWithCheck(skuId, quantity); if (affected 0) { // 查询当前库存供提示 Sku sku skuMapper.selectById(skuId); return Result.fail(String.format(库存不足当前剩余%d, sku.getStock())); } return Result.success(); }关键点有三WHERE条件包含stock #{quantity}数据库层面校验避免扣成负数无SELECT再UPDATE的竞态窗口整个操作原子执行不依赖应用层读-改-写返回affectedRows判断结果affectedRows0即扣减失败前端可立即提示“库存不足”。若需更高并发如秒杀可在此基础上加Redis预减库存但本项目定位中小流量纯DB方案已足够且避免了缓存一致性难题。5.3 规格矩阵生成算法从JSON Schema到HTML表格的零损耗映射frontend/src/utils/sku-matrix.js是前端最硬核的模块它把后端返回的规格定义如[{name:颜色,values:[黑,白]},{name:内存,values:[128,256]}]实时渲染为可编辑表格export function generateSkuMatrix(specs) { if (!specs || specs.length 0) return { headers: [], rows: [] }; // 1. 生成笛卡尔积所有规格组合 const combinations cartesianProduct(specs.map(s s.values)); // 2. 构建表头第一列为规格名后续为各组合 const headers [规格] // 固定首列 .concat(combinations.map((_, i) SKU ${i 1})); // SKU 1, SKU 2... // 3. 构建行数据每行一个规格项每列一个组合的取值 const rows specs.map((spec, specIndex) ({ label: spec.name, values: combinations.map(comb comb[specIndex]) // 取该组合在specIndex位置的值 })); return { headers, rows }; } function cartesianProduct(arrays) { return arrays.reduce((a, b) a.flatMap(d b.map(e [d, e].flat())) ); }SkuTable.vue中调用script setup import { generateSkuMatrix } from /utils/sku-matrix const props defineProps([specs]) const matrix generateSkuMatrix(props.specs) /script template table thead tr th v-forh in matrix.headers :keyh{{ h }}/th /tr /thead tbody tr v-for(row, i) in matrix.rows :keyi td{{ row.label }}/td td v-for(val, j) in row.values :keyj{{ val }}/td /tr /tbody /table /template这套算法的优势在于完全由前端计算不依赖后端返回SKU列表。当运营只配置了规格尚未填价格库存时表格已自动生成所有SKU占位符用户只需在对应单元格填数字——这极大降低了SKU配置门槛也避免了后端因规格组合爆炸导致的内存溢出。6. 二次开发实战如何接入微信支付回调、替换富文本编辑器、增加商品审核流拿到这个系统多数人不是为了跑通Demo而是要快速嫁接到自己的业务中。下面三个高频改造需求我按“改什么→怎么改→验证什么”给出可落地的步骤附真实代码片段。6.1 接入微信支付回调从下单到支付成功闭环业务场景用户下单后需调用微信统一下单API获取prepay_id前端调起支付支付成功后微信服务器异步通知你的/api/pay/notify接口。改什么后端新增PayController处理统一下单与回调前端订单页增加“微信支付”按钮数据库新增order表本项目暂无订单模块需补。怎么改在backend/src/main/java/com/example/shop/controller/下新建PayController.javaRestController RequestMapping(/api/pay) public class PayController { PostMapping(/unified-order) public Result unifiedOrder(RequestBody OrderRequest request) { // 1. 校验订单合法性查商品是否存在、库存是否充足 Sku sku skuMapper.selectById(request.getSkuId()); if (sku null || sku.getStock() request.getQuantity()) { return Result.fail(商品不可用); } // 2. 调用微信统一下单API此处简化实际需引入weixin-java-pay SDK String prepayId wechatService.unifiedOrder( sku.getName(), request.getQuantity() * sku.getPrice(), request.getOrderId() ); return Result.success(Map.of(prepay_id, prepayId)); } PostMapping(/notify) public String notify(RequestBody String xmlData, HttpServletRequest request) { // 3. 解析微信回调XML校验签名更新订单状态 WxPayNotifyResult result WxPayUtil.parseXml(xmlData, WxPayNotifyResult.class); if (SUCCESS.equals(result.getResultCode())) { orderService.updateStatus(result.getOutTradeNo(), PAID); } return xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; } }前端OrderConfirm.vue中调用const handleWechatPay async () { const res await api.pay.unifiedOrder({ skuId: route.query.skuId, quantity: 1, orderId: ORD Date.now() }); // 3. 调起微信JSAPI支付需在公众号后台配置JSAPI域名 WeixinJSBridge.invoke(getBrandWCPayRequest, { appId: wx1234567890, timeStamp: Date.now().toString(), nonceStr: abc, package: prepay_id${res.data.prepay_id}, signType: SHA256, paySign: xxx }, (res) { if (res.err_msg get_brand_wcpay_request:ok) { ElMessage.success(支付成功); router.push(/order/success); } }); };验证什么调用/api/pay/unified-order返回prepay_id微信支付成功后检查数据库order表中对应订单status是否变为PAID查看PayController.notify()日志确认收到XML并解析成功。6.2 替换富文本编辑器从TinyMCE到Quill支持图片拖拽上传业务场景运营抱怨TinyMCE上传图片要点击按钮希望支持拖拽粘贴。改什么前端移除tinymce/tinymce-vue引入quill后端新增图片上传接口/api/upload/image。怎么改安装Quillnpm install quill vueup/vue-quill在GoodsEdit.vue中替换编辑器template !-- 移除 editor /替换为 -- VueQuill v-model:contentform.detail_content themesnow :toolbartoolbarOptions / /template script setup import { VueQuill } from vueup/vue-quill import vueup/vue-quill/dist/vue-quill.snow.css const toolbarOptions [ [bold, italic, underline], [{ list: ordered}, { list: bullet }], [image] // 启用图片按钮 ] // 自定义图片上传Quill默认不支持 onMounted(() { const quill document.querySelector(.ql-editor) quill.addEventListener(drop, handleImageDrop) }) const handleImageDrop (e) { e.preventDefault() const files Array.from(e.dataTransfer.files).filter(f f.type.startsWith(image/)) files.forEach(uploadImage) } const uploadImage (file) { const formData new FormData() formData.append(file, file) api.upload.image(formData).then(res { const img document.createElement(img) img.src res.data.url document.querySelector(.ql-editor).appendChild(img) }) } /script后端UploadController.javaPostMapping(/image) public Result uploadImage(RequestParam(file) MultipartFile file) { try { String fileName UUID.randomUUID() _ file.getOriginalFilename(); Path uploadPath Paths.get(uploads/images/); Files.createDirectories(uploadPath); Files.write(uploadPath.resolve(fileName), file.getBytes()); return Result.success(Map.of(url, /uploads/images/ fileName)); } catch (Exception e) { return Result.fail(上传失败); } }验证什么拖拽图片到编辑器区域自动上传并插入粘贴截图CtrlV同样触发上传查看uploads/images/目录下是否有文件生成。6.3 增加商品审核流三步走从“提交”到“发布”业务场景商品需经运营审核才能上架避免错误信息上线。改什么Goods表增加review_status字段0-未提交1-审核中2-通过3-驳回新增ReviewController处理审核操作前端商品列表页增加“提交审核”“审核通过”按钮。怎么改扩展Goods实体public class Goods { // ...原有字段 private Integer reviewStatus; // 0-草稿,1-已提交,2-已通过,3-已驳回 private Long reviewerId; // 审核人ID private String reviewComment; // 驳回意见 }修改GoodsController.submitReview()PostMapping(/submit-review) public Result submitReview(RequestBody Goods goods) { // 仅允许草稿状态提交 if (goods.getStatus() ! GoodsStatus.DRAFT.getCode()) { return Result.fail(只有草稿商品可提交审核); } goods.setStatus(GoodsStatus.PENDING_REVIEW.getCode()); goods.setReviewStatus(1); // 审核中 goodsMapper.updateById(goods); return Result.success(); }前端GoodsList.vue中增加操作列el-table-column label操作 width200 template #default{ row } el-button v-ifrow.status 0 row.review_status 0 sizesmall clicksubmitReview(row.id) 提交审核/el-button el-button v-ifrow.review_status 1 sizesmall typeprimary clickpassReview(row.id) 审核通过/el-button /template /el-table-column验证什么商品状态为草稿时“提交审核”按钮可见提交后review_status变为1状态仍为草稿status0但列表页显示“审核中”标签审核通过后status变为2已上架review_status变为2。从那以后我每次接手新项目都强制走一遍“状态机校验乐观锁扣减规格矩阵生成”这三关的代码审查。不是因为它们多复杂而是因为90%的线上事故都源于对这三处逻辑的随意绕过。这个网络商品销售管理系统它不炫技不堆砌但每行代码都在回答一个问题“当运营凌晨三点改错一个价格系统会不会崩溃”——答案是不会因为它把确定性刻进了每一处状态流转与数据变更里。希望帮到你。本文还有配套的精品资源点击获取