news 2026/10/31 15:25:03

Spring Boot+MyBatis-Plus商品销售管理系统源码拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot+MyBatis-Plus商品销售管理系统源码拆解

简介:网络商品销售管理系统源码包面向学习电子商务系统开发的学生与初级开发者,也可为需要搭建商品维护、库存预警、订单处理与配送管理流程的商家提供参考。整套代码采用ASP.NET三层结构组织,表示层页面、业务逻辑层管理器、数据访问层服务与实体模型相互分离,并附SQL建库脚本和数据库访问帮助类,便于理解商品查询录入、图片管理、订单整合、退款退货及销售数据分析等模块的实现思路。压缩包共55个文件,以15个dll动态库、13个cs源码、12个pdb调试文件、3个aspx页面、3个csproj项目文件为主,另有txt说明、sql脚本与config配置,整体约88KB,体量轻便,适合本地解压后对照项目结构阅读。已有156人学习,可作为课程设计、毕业设计或小型电商后台原型的参考资料,帮助读者梳理三层架构划分与业务模块之间的调用关系。

1. 从运营那张改错的价目表说起:网络商品销售管理系统要收敛的是什么

运营在 Excel 里把一款鞋的价格从 299 改成 199,只改了其中一个 SKU,第二天客服接了三十个电话。这类事故多半不是人的问题,是商品维护没有约束:价格、库存、上下架状态散在表格和聊天记录里,谁都能改,改完没有留痕。网络商品销售管理系统要做的就是把商品、分类、SKU、库存、订单、客户收进一套 B/S 后台,让商品维护带上校验、状态机和操作记录,改错了还能倒回去查。这套源码适合两类人:想照着跑通一套完整增删改查加权限加订单闭环的初学者,和手里有个小商城、需要一份能直接改的底座的小团队。下面按技术骨架、商品维护链路、跑通步骤、踩坑点、二次开发的顺序拆开讲。

2. 技术选型与工程骨架:Spring Boot + MyBatis-Plus 单模块怎么分层

拿到一份销售管理系统的源码,先别急着点启动类。花十分钟看清楚它用的是什么组合、包是怎么分的、表是怎么设计的,后面改代码能省掉一半的返工。这类项目常见的组合是 Spring Boot + MyBatis-Plus + MySQL + Vue,也有不少是 SSM 老版本或者 Thymeleaf 服务端渲染,映射关系基本一一对应,看懂了分层换哪个版本都能对上号。

2.1 技术栈组合与版本约束

后端用 Spring Boot 2.7.x 打底,JDK 8 或 11 都能跑,别硬上 17,很多老依赖在 17 上会因为反射限制报错。持久层选 MyBatis-Plus 3.5.x,理由很实在:商品维护模块九成是单表条件分页加批量更新,用 JPA 要写一堆 Specification,用原生 MyBatis 要手写几十个 XML,MyBatis-Plus 的LambdaQueryWrapper能把条件拼装压到几行,还能顺手拿到逻辑删除和自动填充时间。

MySQL 用 8.0,字符集统一utf8mb4,排序规则utf8mb4_general_ci。Redis 在这类项目里通常是可选项,常见做法是只干两件事:缓存商品分类树、存登录 token 或 token 黑名单。别一上来就给商品列表加缓存,商品改一次要失效一堆 key,很容易出现「后台改了价格前台还是老价格」的问题,这是新手最容易踩的坑之一。

前端是 Vue 2.6 + Element UI + axios,用 vue-cli 起工程,Node 建议 14 或 16。权限部分看源码包,有的是 Spring Security + JWT,有的是自己写的拦截器 + token 表,后者的可读性对二次开发更友好,改动权限规则不用去翻过滤器链。下面讲的结构按「拦截器 + token」这套来,如果你手上是 Security 版本,把拦截器那部分换成配置类就等价。

2.2 目录结构与分层职责

单模块分包,比多模块 Maven 更适合这种体量的项目,改一个字段不用在四个 pom 之间来回跳。典型结构是这样:

mall-admin ├── src/main/java/com/example/mall │ ├── MallApplication.java # 启动类,@MapperScan 在这 │ ├── common/ # Result、PageQuery、BusinessException、全局异常 │ ├── config/ # MybatisPlusConfig、WebMvcConfig、RedisConfig │ ├── interceptor/ # AuthInterceptor:登录态 + 接口权限校验 │ ├── module │ │ ├── product/ # controller / service / mapper / entity / dto / vo │ │ ├── category/ │ │ ├── order/ │ │ └── sys/ # 用户、角色、菜单 │ └── util/ # 上传、Excel、树形结构工具 ├── src/main/resources │ ├── mapper/ # 复杂查询的 XML 放这里 │ ├── application.yml │ └── application-dev.yml └── sql/ # schema.sql、data.sql

按业务域分包而不是按 controller/service/mapper 三层平铺,好处很直接:要找商品维护的代码,进module/product就够了,不用在三个包之间来回切。每个域内部再分 controller、service、mapper,各层职责别混:Controller 只做参数接收和返回体包装,业务规则全部压到 Service,Mapper 层最多写复杂 SQL,不要在 XML 里塞if业务判断。DTO 和 VO 分开,ProductSaveDTO收前端入参,ProductVO给前端出参,实体Product只跟表对齐,三者混用是后面字段一多就失控的根源。

2.3 核心表设计:商品、SKU、分类、库存流水

商品维护模块能不能扛住业务,八成取决于表设计。这里最关键的一条是 SPU 和 SKU 拆开:同一款鞋是一个 SPU,它的 42 码黑、43 码白是两条 SKU,价格和库存在 SKU 上,标题和主图在 SPU 上。不拆的话,运营加一个尺码就得复制一条完整的商品记录,改标题要改 N 遍。

CREATE TABLE `pms_category` ( `id` bigint NOT NULL AUTO_INCREMENT, `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '0 表示一级分类', `name` varchar(64) NOT NULL, `level` tinyint NOT NULL DEFAULT 1 COMMENT '层级,只允许末级挂商品', `sort` int NOT NULL DEFAULT 0, `deleted` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_parent` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类'; CREATE TABLE `pms_product` ( `id` bigint NOT NULL AUTO_INCREMENT, `category_id` bigint NOT NULL COMMENT '末级分类 ID', `name` varchar(128) NOT NULL, `sub_title` varchar(255) DEFAULT NULL, `main_pic` varchar(255) DEFAULT NULL COMMENT '存相对路径,别存绝对路径', `price` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '展示价,取 SKU 最低价', `publish_status` tinyint NOT NULL DEFAULT 0 COMMENT '0 下架 1 上架', `deleted` bigint NOT NULL DEFAULT 0 COMMENT '0 未删,非 0 为删除时间戳', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品 SPU'; CREATE TABLE `pms_sku` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint NOT NULL, `sku_code` varchar(64) NOT NULL COMMENT '全局唯一,导入时用它去重', `specs` varchar(255) DEFAULT NULL COMMENT '颜色:黑;尺码:42', `price` decimal(10,2) NOT NULL, `stock` int NOT NULL DEFAULT 0, `lock_stock` int NOT NULL DEFAULT 0 COMMENT '下单未支付锁定量', `version` int NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_sku_code` (`sku_code`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品 SKU'; CREATE TABLE `pms_stock_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `sku_id` bigint NOT NULL, `change_num` int NOT NULL COMMENT '正数入库,负数出库', `biz_type` varchar(32) NOT NULL COMMENT 'INIT/ORDER/DELIVER/REFUND', `biz_no` varchar(64) DEFAULT NULL COMMENT '业务单号,便于对账', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_sku` (`sku_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水';

几个字段值得单独说。价格一律decimal(10,2),别用double和图省事,0.1 + 0.2这种精度问题在结算页会变成实打实的一分钱对不上。deleted用bigint存时间戳而不是tinyint存 0/1,是为了绕开唯一索引冲突,后面避坑那章会细说。库存流水表看着多余,但真出问题时它是唯一能查的「后悔药」——某天库存对不上,翻流水比翻日志快得多。

3. 商品维护模块拆解:一个 SPU 从新增到上架的完整链路

商品维护是这套系统里改得最频繁的模块,也是二次开发时第一个要动的地方。这一段按「实体映射 → 新增校验 → 分页查询 → 上下架状态机 → 批量导入」走一遍,每段都给出可以直接抄的代码。

3.1 实体映射与字段约定

实体类和表对齐,但注解要写对,否则逻辑删除、自动填充、主键策略都会失效。

@Data @TableName("pms_product") public class Product { @TableId(type = IdType.AUTO) // 自增,导入历史数据时可保留原 ID private Long id; private Long categoryId; private String name; private String subTitle; private String mainPic; private BigDecimal price; private Integer publishStatus; @TableLogic(value = "0", delval = "UNIX_TIMESTAMP()") private Long deleted; // 删除时写入时间戳,天然不冲突 @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }

@TableLogic的delval直接写UNIX_TIMESTAMP(),MyBatis-Plus 会把它当原生 SQL 拼进 update 语句,删一次写一个秒级时间戳,同一个名字反复删也不会撞唯一索引。自动填充需要在config下注册一个MetaObjectHandler,在insertFill和updateFill里分别塞LocalDateTime.now(),忘了注册的话create_time永远是 null,后面按时间排序全乱。

3.2 新增与编辑的校验:把脏数据挡在 Service 层

DTO 上的@NotBlank、@Min只能拦住格式错误,业务规则必须落在 Service,因为唯一性、分类层级这些都要查库。

@Service @RequiredArgsConstructor public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService { private final CategoryMapper categoryMapper; private final SkuMapper skuMapper; @Override @Transactional(rollbackFor = Exception.class) public Long saveProduct(ProductSaveDTO dto) { // 1. 分类必须是末级,避免商品挂在有子节点的分类上导致前台筛选查不到 Category category = categoryMapper.selectById(dto.getCategoryId()); if (category == null || category.getLevel() < 2) { throw new BusinessException("请选择末级分类"); } // 2. 同分类下名称不许重复(并发靠唯一索引兜底,见第 5 章) Long same = lambdaQuery() .eq(Product::getCategoryId, dto.getCategoryId()) .eq(Product::getName, dto.getName()) .count(); if (same > 0) { throw new BusinessException("该分类下已存在同名商品"); } // 3. 至少要有一条 SKU,且价格大于 0 if (CollectionUtils.isEmpty(dto.getSkuList())) { throw new BusinessException("请至少添加一条 SKU"); } dto.getSkuList().forEach(sku -> { if (sku.getPrice() == null || sku.getPrice().compareTo(BigDecimal.ZERO) <= 0) { throw new BusinessException("SKU 价格必须大于 0"); } }); Product product = BeanUtil.copyProperties(dto, Product.class); product.setPublishStatus(0); // 新建一律下架,人工上架 product.setPrice(dto.getSkuList().stream() .map(SkuSaveDTO::getPrice).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO)); save(product); for (SkuSaveDTO s : dto.getSkuList()) { Sku sku = BeanUtil.copyProperties(s, Sku.class); sku.setProductId(product.getId()); skuMapper.insert(sku); // 初始库存走流水,保证任何库存变动都能追溯到来源 if (sku.getStock() != null && sku.getStock() > 0) { stockService.record(sku.getId(), sku.getStock(), "INIT", null); } } return product.getId(); } }

rollbackFor = Exception.class一定要写。默认 Spring 只在RuntimeException和Error上回滚,自定义的受检异常抛出去事务不回滚,商品插进去了 SKU 没插进去,就成了脏数据。方法里三次抛异常的位置也值得注意:分类校验必须在写库之前,SKU 循环里的校验必须在一个事务内,否则前两条 SKU 已经落库,第三条报错就回滚不掉了。

3.3 列表查询:分页、多条件与分类树过滤

后台列表的诉求通常是「按名字搜、按分类筛、按状态筛,还要带分页」,分类筛选得把子分类一起带上,不然选一级分类查不到东西。

@Override public IPage<ProductVO> pageQuery(ProductQuery query) { List<Long> categoryIds = categoryService.selfAndChildrenIds(query.getCategoryId()); LambdaQueryWrapper<Product> wrapper = Wrappers.<Product>lambdaQuery() .like(StringUtils.hasText(query.getName()), Product::getName, query.getName()) .in(CollectionUtils.isNotEmpty(categoryIds), Product::getCategoryId, categoryIds) .eq(query.getPublishStatus() != null, Product::getPublishStatus, query.getPublishStatus()) .orderByDesc(Product::getId); Page<Product> page = page(new Page<>(query.getPageNum(), query.getPageSize()), wrapper); return page.convert(p -> { ProductVO vo = BeanUtil.copyProperties(p, ProductVO.class); vo.setCategoryName(categoryService.nameOf(p.getCategoryId())); vo.setTotalStock(skuMapper.sumStockByProductId(p.getId())); return vo; }); }

selfAndChildrenIds内部是先查全量分类再在内存里递归,分类表通常几百条,全量捞一次加 Redis 缓存比递归查库快得多。分页这里有个隐性依赖:PaginationInnerInterceptor必须注册,否则page()返回的total恒为 0,前端翻页永远停在第 1 页——这个坑几乎每个用 MyBatis-Plus 的人都翻过。

3.4 上下架状态机与库存扣减写法

上下架不是简单改个字段,上架前得确认主图有、有货、价格合法,否则前台会出现「点进去 404」或者「下单提示无库存」。

@Override public void publish(Long id, Integer status) { Product product = getById(id); if (product == null) { throw new BusinessException("商品不存在"); } if (Objects.equals(status, product.getPublishStatus())) { return; // 幂等:重复点击直接返回 } if (status == 1) { if (!StringUtils.hasText(product.getMainPic())) { throw new BusinessException("缺少主图,不能上架"); } Long sellable = skuMapper.selectCount(Wrappers.<Sku>lambdaQuery() .eq(Sku::getProductId, id).gt(Sku::getStock, 0)); if (sellable == 0) { throw new BusinessException("没有可售库存的 SKU,不能上架"); } } // 带原状态做条件,防止两个运营同时操作导致状态互相覆盖 boolean ok = lambdaUpdate() .eq(Product::getId, id) .eq(Product::getPublishStatus, product.getPublishStatus()) .set(Product::getPublishStatus, status) .update(); if (!ok) { throw new BusinessException("状态已被他人修改,请刷新后重试"); } }

扣库存的写法比select ... for update更常用,直接靠 SQL 的条件更新做 CAS:

@Update("update pms_sku set stock = stock - #{num}, version = version + 1 " + "where id = #{skuId} and stock >= #{num}") int deductStock(@Param("skuId") Long skuId, @Param("num") Integer num);

返回 0 就说明库存不够或者被别人抢先扣了,Service 里直接抛异常让事务回滚。这种写法不用持行锁等待,并发下单时吞吐比悲观锁高一截,代价是扣减失败要由上层重试或者提示用户。要改成本地消息表配合重试也容易,deductStock的入参不变。

3.5 批量导入:按行独立事务

运营最爱的功能是「给我一个 Excel 我一次传两千条」。整批一个事务的话,第 1999 行报错前 1998 行全白干,所以得按行提交。

public ImportResult importProducts(MultipartFile file) throws IOException { List<ProductImportRow> rows = EasyExcel.read(file.getInputStream()) .head(ProductImportRow.class).sheet().doReadSync(); List<String> errors = new ArrayList<>(); int success = 0; for (int i = 0; i < rows.size(); i++) { try { // 必须走代理对象,this.saveProduct 会让 @Transactional 失效 self.saveProduct(rows.get(i).toDto()); success++; } catch (Exception e) { errors.add("第 " + (i + 2) + " 行:" + e.getMessage()); // +2 是表头偏移 } } return new ImportResult(success, errors); }

self是从容器里注入的自身代理(@Lazy注入或者AopContext.currentProxy()),这点在批量导入里是硬性要求。用this调用的话,被调用方法上的@Transactional完全不生效,等于整批共用一个外层事务,一行失败全批回滚,而且日志里看不出任何异常提示,排查起来非常费劲。

4. 本地跑通:建库脚本、配置文件与启动后的冒烟验证

代码看明白之后就该让它转起来。这段按顺序给命令,照着敲能在一个新环境里从零跑到后台登录。

4.1 建库与初始化数据

# 1. 建库,字符集必须 utf8mb4,否则商品名里的生僻字和 emoji 会变成问号 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS mall_admin \ DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 2. 建表 + 灌基础数据(分类、admin 账号、角色菜单) mysql -uroot -p mall_admin < sql/schema.sql mysql -uroot -p mall_admin < sql/data.sql

导入前先看一眼schema.sql里有没有DROP TABLE,有些源码包会在开头无条件删表,本地跑第二次时把你自己造的数据一并清了。这一步做完用show tables数一下表数量,跟sql/目录里对不上的话,多半是脚本里有多条语句被客户端截断了。

4.2 配置文件:数据源、Redis、上传路径、跨域

server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mall_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: ${DB_PASSWORD:123456} driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mall: upload-path: /data/mall/upload/ # 物理路径,末尾必须带斜杠 upload-url-prefix: /files/ # 对外访问前缀 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-not-delete-value: 0 logic-delete-value: UNIX_TIMESTAMP()

serverTimezone=Asia/Shanghai不能省,不写的话 MySQL 8 驱动会按 UTC 解析,后台显示的下单时间会比实际早 8 小时,运营看报表时会以为数据丢了。upload-path用绝对路径,别写./upload/,打成 jar 之后工作目录会变,图片能上传成功但刷新就 404。密码用${DB_PASSWORD:123456}这种占位符,本地方便,上服务器时用环境变量覆盖,不用改代码。

4.3 启动与接口冒烟

mvn -q clean package -DskipTests java -jar target/mall-admin-1.0.0.jar --spring.profiles.active=dev

启动日志里看到 Tomcat 起在 8080 之后,先别急着开前端。用两条命令验证后端是通的:

# 登录拿 token,jq 只是为了截字段,没有可以手动复制 TOKEN=$(curl -s -X POST http://127.0.0.1:8080/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"123456"}' | jq -r '.data.token') # 带 token 查商品分页,返回 code=200 且 total 有值就算通 curl -s "http://127.0.0.1:8080/api/product/page?pageNum=1&pageSize=10" \ -H "Authorization: $TOKEN"

total是 0 而records是空数组,先去看分页插件有没有注册;接口直接返 401,看拦截器里 token 是从 header 还是从 cookie 取的,跟前端传的方式要对上。

4.4 前端联调与静态资源映射

前端装完依赖npm run serve,改vue.config.js的代理,别写死 IP,不然换台机器就得改一遍:

module.exports = { devServer: { port: 9528, proxy: { '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true } } } }

图片访问要靠后端做静态资源映射,否则上传成功了但<img>全是红叉:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${mall.upload-path}") private String uploadPath; @Value("${mall.upload-url-prefix}") private String prefix; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(prefix + "**") .addResourceLocations("file:" + uploadPath); } }

file:后面跟的必须是目录且以斜杠结尾,写成file:/data/mall/upload少一个斜杠,Spring 会把它当成文件路径,结果就是所有图片 404。

5. 避坑与排查:商品维护链路上最容易翻车的 5 个点

这些坑基本都出在「代码看着没问题但结果不对」的地方,按现象、原因、解决三段记,出问题时对着查。

现象一:分页查出来 total 永远是 0,前端翻页一直停在第 1 页。原因是没有注册PaginationInnerInterceptor,MyBatis-Plus 不会自动帮你拼count语句,SQL 里没有 limit 也不会有总数。解决办法是在配置类里显式加上:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }

顺带提一句,Page参数页码从 1 开始,前端如果传 0 会查出空列表,这种问题在联调时最容易被当成「接口没数据」。

现象二:同一个分类下删掉一个商品,再新建同名商品报唯一索引冲突。原因是唯一索引建在了(category_id, name)上,而逻辑删除只把deleted从 0 改成 1,被删的那行还在表里,索引照样算数。解决有两种:一是把deleted改成bigint存时间戳并加进唯一索引(category_id, name, deleted),删两次时间戳不同就不冲突;二是不建唯一索引,靠 Service 的 count 校验兜底,但要接受并发下可能出现的重复数据。这类项目里我一般选第一种,改表比改并发逻辑便宜。

现象三:后台改完价格,前台列表还是老价格,要等好几分钟才变。原因是给商品列表加了 Redis 缓存,但更新商品时只delete了详情 key,忘了清列表 key,或者列表 key 里带分页参数导致根本清不干净。解决办法是商品这类会频繁变更的数据干脆不加缓存,或者按category_id维度打标签,更新时按标签批量失效。缓存这东西在后台系统里收益远没有想象中大,命中率低还带来一致性问题,属于典型的「加了后悔,不加也没事」。

现象四:批量导入两千条商品,前面成功后面全部回滚,日志里一条报错都没有。原因是循环里用this.saveProduct(...)调用,@Transactional走的是类内调用不经过代理,事务注解直接失效,所有行共用外层事务。解决是把自身代理注入进来@Lazy private ProductService self;,或者用AopContext.currentProxy(),改完再跑一遍,错误行会单独回滚并出现在errors列表里。这个问题不看源码几乎发现不了,属于最难查的一类问题。

现象五:商品上架成功,但前台下单提示「库存不足」,后台显示还有货。原因是库存扣减只扣了stock没扣lock_stock,或者下单锁库存和支付扣库存写了两套逻辑,其中一套漏了。解决办法是把所有库存变动收敛到一个StockService,任何地方改库存都走它,同时写pms_stock_record流水。库存对不上时,直接按sku_id把流水加起来和当前库存比,差多少、哪一步少的,一查就清楚,比翻应用日志快得多。

6. 二次开发:把商品维护改成贴合自己业务的字段与规则

真正落地时,很少有人能原封不动用一套现成的商品维护。常见的改造是加字段、加规格维度、加上架审核。加字段这件事有个顺序别搞反:先改表,再改实体,再改 DTO/VO,最后改前端表单和列表列,反着来会一路报错。

改造点要动的文件容易忽略的地方
加品牌、产地、保质期schema.sql、Product、ProductSaveDTO、ProductVO、前端表单VO 忘了加字段,前端拿不到值以为没保存
加规格维度(颜色/尺码变三档)pms_sku.specs改成 JSON,或拆pms_sku_spec表老数据specs是分号串,要做一次数据迁移
上架改成先审核Product加audit_status,publish()拆成提交和审核两个方法审核通过前前台不能查出来,列表 SQL 要加条件
接外部 ERP 同步商品新增ProductSyncService,走定时任务或消息同步要按sku_code幂等,重复推送不能插重

加规格维度我踩过一次坑:直接给specs字段塞 JSON,前端渲染没问题,但分类筛选那块的 SQL 里用了like '%红%',结果「红色」和「红棕色」互相污染。后来改成拆表,pms_sku_spec(sku_id, spec_key, spec_value),筛选走exists子查询才干净。改造前评估一下查询场景,纯展示的话 JSON 够用,要做组合筛选就得拆表,这个决定后面很难改。

验证改造是否生效,别只点页面。写完一个字段我一般走三步:先用curl打一次新增接口,看返回的 ID 和数据库里那行对不对;再查一次分页接口,确认 VO 里新字段有值;最后才开前端看表单回显。页面能过但接口挂的情况太多了,接口层过了前端基本不会出问题。

从那以后我每次动商品表结构,都强制先跑一遍「建库脚本 → 启动 → 新增一条 → 查列表 → 上架 → 下单」这条完整链路,中间任何一步失败就停下来查,不等攒到最后一起调。这个习惯帮我省掉了不少通宵。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 15:22:49

MySQL 8.0 免安装版部署指南:ZIP包手动初始化与Windows服务配置

简介&#xff1a;本资源为 MySQL 8.0 免安装绿色版&#xff0c;面向数据库初学者、开发测试人员及轻量级项目部署需求者&#xff0c;解决传统 MySQL 安装配置繁琐、环境依赖复杂、启动调试耗时等问题。压缩包共420个文件&#xff0c;主体包含98个DLL动态库&#xff08;支撑服务…

作者头像 李华
网站建设 2026/10/31 15:21:58

亚马逊云免费服务器搭建指南:从注册到避坑的完整实操

云服务器这件事&#xff0c;我踩过的坑比大多数人走过的路都多。早些年自掏腰包买过好几家海外小厂的低价机器&#xff0c;结果不是三天两头宕机&#xff0c;就是账单莫名其妙翻倍。后来学乖了&#xff0c;开始认真研究各家云厂商的免费额度&#xff0c;其中亚马逊云&#xff0…

作者头像 李华
网站建设 2026/10/31 15:21:03

图片无损放大真相:高质量重建的技术路径与场景化实践

1. 先破个题&#xff1a;所谓“无损放大”&#xff0c;从来就不存在很多人第一次听说“图片无损放大”&#xff0c;是在某款手机修图App里点开“超分”按钮的那一刻——原图200KB&#xff0c;放大2倍后依然清晰锐利&#xff0c;边缘没锯齿、纹理没糊成一片&#xff0c;连发丝都…

作者头像 李华
网站建设 2026/10/31 15:21:01

实时云渲染如何支撑元宇宙品牌营销2.0

1. 项目概述&#xff1a;当品牌营销不再依赖“等渲染完成”&#xff0c;而是“所见即所得”“元境实时云渲染技术助力元宇宙品牌营销进入2.0新时代”——这个标题里藏着三个被行业反复验证却长期难以落地的痛点&#xff1a;实时性、一致性、可规模化。我接触过二十多个品牌方的…

作者头像 李华
网站建设 2026/10/31 15:20:57

学生成绩管理系统数据库设计:从E-R建模到触发器与答辩验收

简介&#xff1a;这份文档是北邮研一数据库课程大作业的完整详解&#xff0c;面向需要完成学生成绩管理系统课程设计的高校学生&#xff0c;尤其适合正在准备数据库大作业、课程设计或答辩复现的读者。内容围绕成绩信息维护、教师信息管理、不及格学生统计及无教学任务教师查询…

作者头像 李华
网站建设 2026/10/31 15:20:02

横沟正史电子书全集整理:epub与mobi双格式的版本甄别与排版修复

电子书收藏这件事&#xff0c;我折腾了快八年&#xff0c;从最早用MP3大小的屏幕啃TXT&#xff0c;到后来折腾各种阅读器、转换格式、修复排版&#xff0c;踩过的坑能写满一个笔记本。今天聊的这套横沟正史作品全集&#xff0c;是我个人数字书库里比较有代表性的一次整理项目—…

作者头像 李华