尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot商品管理实战:扩展属性、状态流转与逻辑删除

Spring Boot商品管理实战:扩展属性、状态流转与逻辑删除 在做电商后台或者商品管理系统的时候最容易被低估的模块就是“商品信息维护”。表面上看只是对几个字段做增删改查真正涉及 SKU、扩展属性、上下架、失效状态、逻辑删除之后设计好坏直接决定后续迭代是不是顺畅。我最近在整理一套家居类商品的后台演示项目时借着一款“可拆洗布艺沙发床”的完整商品信息把商品档案管理的核心流程完整做了一遍。这篇文章就围绕这套实现展开从需求拆解、数据模型、Spring Boot 后端到 Thymeleaf 页面展示和状态流转一步步还原一个带有真实业务感的商品管理系统新手可以直接跟着做正在做毕设或者公司小项目的朋友也能拿来参考。1. 业务背景商品状态管理为什么容易写乱1.1 一条商品信息里到底藏了多少内容我们以一款沙发床商品为例它看起来只是一条商品记录真正在后台建模的时候会发现信息需要拆成两个层次。第一层是商品的核心档案包括商品名称、品牌、型号、分类、风格、尺寸、价格、库存、状态等。这一部分相对固定是商品的主键信息在列表中展示、在详情页回显也都依赖这些字段。第二层是商品的可变属性也就是不同品类差异最大的部分。比如沙发床会有“是否可拆洗”、“面料成分”、“填充物”、“环保等级”、“甲醛含量”这些属性而手机类商品则是“运行内存”、“存储容量”、“屏幕尺寸”。如果把这些属性全部做成主表字段数据库每次加属性都要改表结构非常被动。所以我在商品主表之外增加了“扩展属性表”用 key-value 的方式保存“材质雪尼尔”、“环保等级母婴A类”、“甲醛含量0”这类信息。这样主表结构稳定商品属性又足够灵活等于为不同类目预留了扩展空间。1.2 “已失效”不只是删除标题中有一个词很关键已失效。在真实电商后台里商品往往不是直接被物理删除的而是先进入“下架”、“失效”、“停售”等业务状态。已失效和删除的区别在于已失效是业务状态商品数据还在管理员可以查看历史记录、分析原因、重新上架或归档。删除是数据操作一旦物理删除后续无法追溯。因此本文的示例系统会把商品状态设计为三种上架、下架、已失效。同时再增加一个逻辑删除标记真正删除时只把deleted字段置为true查询列表时统一过滤。这样既满足业务需要也保留了追溯能力。1.3 本项目的整体功能目标完整功能拆分如下商品列表展示支持按状态观察商品。商品新增和编辑包含核心字段填写和扩展属性维护。商品详情页面展示完整基础信息与属性列表。上架、下架、标记失效三个状态操作按钮。逻辑删除数据不物理移除。属性文本按行解析为结构化属性便于在页面以表格展示。2. 技术选型与环境准备2.1 技术栈说明本文示例采用 Spring Boot 作为后端基础框架使用 Spring Data JPA 操作数据库使用 Thymeleaf 作为服务端模板引擎。选择这套组合的原因很简单Spring Boot 能快速搭建可运行的 Web 应用适合示例项目。Spring Data JPA 可以通过方法名自动生成 SQL减少样板代码。Thymeleaf 与 Spring Boot 集成好页面可以直接展示后端数据。H2 数据库作为内存数据库不需要额外安装环境适合本地演示。2.2 环境版本示例代码以常见环境为例不同版本可能会有细微差异重点看设计思路JDK17Spring Boot2.7.18构建工具Maven数据库H2 内存模式模板引擎ThymeleafIDEIntelliJ IDEA 或 Eclipse 均可Spring Boot 2.7 对应的是javax.*命名空间如果你使用 Spring Boot 3.x需要将部分包名替换为jakarta.*这个差异在引入代码时要特别注意。2.3 项目结构规划项目整体按常见的分层结构组织product-manage ├── pom.xml └── src/main ├── java/com/example/product │ ├── ProductApplication.java │ ├── entity │ │ ├── Product.java │ │ ├── ProductAttribute.java │ │ └── ProductStatus.java │ ├── repository │ │ └── ProductRepository.java │ ├── service │ │ └── ProductService.java │ └── controller │ └── ProductController.java └── resources ├── application.yml └── templates/product ├── list.html ├── form.html └── detail.htmlentity 放实体类repository 放数据访问层service 放业务逻辑controller 放页面跳转和请求处理。模板放在templates/product目录下由 Thymeleaf 自动解析。3. 数据库与实体设计3.1 表结构设计数据库一共两张表。商品主表product字段类型说明idbigint主键自增namevarchar商品名称brandvarchar品牌modelvarchar型号categoryvarchar分类stylevarchar风格sizevarchar尺寸pricedecimal价格stockint库存statusvarchar商品状态见枚举descriptiontext商品描述deletedboolean逻辑删除标记create_timetimestamp创建时间update_timetimestamp更新时间商品扩展属性表product_attribute字段类型说明idbigint主键自增product_idbigint关联商品主表attr_namevarchar属性名称attr_valuevarchar属性值sortint排序号一张主表配一张扩展属性表是商品系统里很经典的设计方式。主表保证稳定的核心字段扩展表支持不同类目的灵活属性。3.2 状态枚举说明状态字段使用枚举的字符串形式保存到数据库public enum ProductStatus { ON_SALE(上架), OFF_SALE(下架), INVALID(已失效); private final String desc; ProductStatus(String desc) { this.desc desc; } public String getDesc() { return desc; } }保存到数据库时使用EnumType.STRING数据库中保存的是ON_SALE、OFF_SALE、INVALID。这样比保存枚举下标更安全因为枚举顺序一旦调整历史数据不会错乱。3.3 商品主表实体类// 文件路径src/main/java/com/example/product/entity/Product.java package com.example.product.entity; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; Data NoArgsConstructor AllArgsConstructor Entity Table(name product) public class Product { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private String name; private String brand; private String model; private String category; private String style; private String size; private BigDecimal price; private Integer stock; Enumerated(EnumType.STRING) private ProductStatus status; Column(columnDefinition TEXT) private String description; private Boolean deleted false; private LocalDateTime createTime; private LocalDateTime updateTime; OneToMany(mappedBy product, cascade CascadeType.ALL, orphanRemoval true) OrderBy(sort asc) private ListProductAttribute attributes new ArrayList(); PrePersist public void prePersist() { this.createTime LocalDateTime.now(); this.updateTime LocalDateTime.now(); if (this.deleted null) { this.deleted false; } } PreUpdate public void preUpdate() { this.updateTime LocalDateTime.now(); } }需要注意OneToMany中配置了cascade CascadeType.ALL表示保存或更新商品时扩展属性会一起保存或更新。orphanRemoval true表示当集合中某个属性被移除后数据库中的对应记录会被删除。这是保证属性更新不残留脏数据的关键。3.4 扩展属性实体类// 文件路径src/main/java/com/example/product/entity/ProductAttribute.java package com.example.product.entity; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import javax.persistence.*; Data NoArgsConstructor AllArgsConstructor Entity Table(name product_attribute) public class ProductAttribute { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private String attrName; Column(nullable false) private String attrValue; private Integer sort; ManyToOne(fetch FetchType.LAZY) JoinColumn(name product_id) private Product product; }属性表通过product_id外键关联商品主表。ManyToOne使用懒加载避免查询列表时把每个商品的整个属性列表都加载出来减少不必要的性能开销。4. 完整实战从零搭建商品信息管理系统4.1 创建 Maven 工程与 pom.xml新建一个 Maven 项目项目名可以设置为product-manage。pom.xml完整内容如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIdproduct-manage/artifactId version1.0.0/version nameproduct-manage/name description商品信息管理示例项目/description properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project其中spring-boot-starter-web提供 Web 容器和 Spring MVC 能力spring-boot-starter-data-jpa提供 JPA 操作能力spring-boot-starter-thymeleaf提供模板页面渲染能力h2是本地内存数据库。4.2 编写 application.yml在src/main/resources下创建application.ymlserver: port: 8080 spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false h2: console: enabled: true path: /h2-console thymeleaf: cache: false这里说明几个配置的作用ddl-auto: update表示启动时根据实体自动建表或更新表结构适合学习和本地演示。show-sql: true会在控制台输出 JPA 执行的 SQL方便排查问题。open-in-view: false是 Spring Boot 官方推荐的设置避免在视图渲染阶段意外持有数据库连接。DB_CLOSE_DELAY-1保证内存数据库在应用运行期间不会因为连接关闭而丢失数据。thymeleaf.cache: false让页面模板修改后立即生效适合开发阶段。4.3 编写启动类// 文件路径src/main/java/com/example/product/ProductApplication.java package com.example.product; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }4.4 编写 Repository// 文件路径src/main/java/com/example/product/repository/ProductRepository.java package com.example.product.repository; import com.example.product.entity.Product; import com.example.product.entity.ProductStatus; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; public interface ProductRepository extends JpaRepositoryProduct, Long { ListProduct findByDeletedFalseOrderByUpdateTimeDesc(); ListProduct findByStatusAndDeletedFalse(ProductStatus status); OptionalProduct findByIdAndDeletedFalse(Long id); }这里关键点是所有查询方法都带有DeletedFalse条件保证逻辑删除的数据不会出现在业务查询结果中。findByIdAndDeletedFalse返回Optional调用方可以明确判断商品是否存在。4.5 编写 Service 层Service 层承载核心业务逻辑包括新增、修改、状态流转、逻辑删除、属性解析。// 文件路径src/main/java/com/example/product/service/ProductService.java package com.example.product.service; import com.example.product.entity.Product; import com.example.product.entity.ProductAttribute; import com.example.product.entity.ProductStatus; import com.example.product.repository.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; Service RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; Transactional(readOnly true) public ListProduct listAvailable() { return productRepository.findByDeletedFalseOrderByUpdateTimeDesc(); } Transactional(readOnly true) public Product getDetail(Long id) { return productRepository.findByIdAndDeletedFalse(id) .orElseThrow(() - new IllegalArgumentException(商品不存在或已删除)); } Transactional public Product createProduct(Product product, String attributeText) { if (product.getStatus() null) { product.setStatus(ProductStatus.OFF_SALE); } product.setDeleted(false); product.setAttributes(parseAttributes(attributeText, product)); return productRepository.save(product); } Transactional public Product updateProduct(Long id, Product form, String attributeText) { Product product getDetail(id); product.setName(form.getName()); product.setBrand(form.getBrand()); product.setModel(form.getModel()); product.setCategory(form.getCategory()); product.setStyle(form.getStyle()); product.setSize(form.getSize()); product.setPrice(form.getPrice()); product.setStock(form.getStock()); product.setDescription(form.getDescription()); product.setStatus(form.getStatus()); product.getAttributes().clear(); product.getAttributes().addAll(parseAttributes(attributeText, product)); return productRepository.save(product); } Transactional public void changeStatus(Long id, ProductStatus status) { Product product getDetail(id); product.setStatus(status); } Transactional public void softDelete(Long id) { Product product getDetail(id); product.setDeleted(true); product.setStatus(ProductStatus.INVALID); } private ListProductAttribute parseAttributes(String text, Product product) { ListProductAttribute list new ArrayList(); if (text null || text.trim().isEmpty()) { return list; } String[] pairs text.split(;); for (String pair : pairs) { if (!pair.contains()) { continue; } String[] kv pair.split(, 2); ProductAttribute attribute new ProductAttribute(); attribute.setAttrName(kv[0].trim()); attribute.setAttrValue(kv[1].trim()); attribute.setSort(list.size()); attribute.setProduct(product); list.add(attribute); } return list; } }Service 层有两个地方值得重点关注。第一所有写操作都加了Transactional保证一个操作要么全部成功要么全部回滚。比如更新商品时先更新主表再清理属性、插入属性如果中途出错事务会回滚到更新前状态不会出现主表数据变了但属性没变的情况。第二parseAttributes方法把页面上传入的“属性值;属性值”文本解析成实体集合。新增时直接设置到商品实体更新时先clear()再addAll()配合前文实体类中的orphanRemoval true可以让旧属性先删除、新属性再插入保持数据库干净。4.6 编写 Controller// 文件路径src/main/java/com/example/product/controller/ProductController.java package com.example.product.controller; import com.example.product.entity.Product; import com.example.product.entity.ProductStatus; import com.example.product.service.ProductService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.stream.Collectors; Controller RequestMapping(/products) RequiredArgsConstructor public class ProductController { private final ProductService productService; GetMapping public String list(Model model) { model.addAttribute(products, productService.listAvailable()); return product/list; } GetMapping(/{id}) public String detail(PathVariable Long id, Model model) { model.addAttribute(product, productService.getDetail(id)); return product/detail; } GetMapping(/new) public String createPage(Model model) { model.addAttribute(product, new Product()); model.addAttribute(statusList, ProductStatus.values()); model.addAttribute(attributeText, ); return product/form; } PostMapping public String create(Valid ModelAttribute Product product, RequestParam String attributeText, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { model.addAttribute(statusList, ProductStatus.values()); model.addAttribute(attributeText, attributeText); return product/form; } Product saved productService.createProduct(product, attributeText); return redirect:/products/ saved.getId(); } GetMapping(/{id}/edit) public String editPage(PathVariable Long id, Model model) { Product product productService.getDetail(id); model.addAttribute(product, product); model.addAttribute(statusList, ProductStatus.values()); model.addAttribute(attributeText, buildAttributeText(product)); return product/form; } PostMapping(/{id}/update) public String update(PathVariable Long id, Valid ModelAttribute Product product, RequestParam String attributeText, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { model.addAttribute(statusList, ProductStatus.values()); model.addAttribute(attributeText, attributeText); return product/form; } productService.updateProduct(id, product, attributeText); return redirect:/products/ id; } PostMapping(/{id}/onSale) public String onSale(PathVariable Long id) { productService.changeStatus(id, ProductStatus.ON_SALE); return redirect:/products/ id; } PostMapping(/{id}/offSale) public String offSale(PathVariable Long id) { productService.changeStatus(id, ProductStatus.OFF_SALE); return redirect:/products/ id; } PostMapping(/{id}/invalidate) public String invalidate(PathVariable Long id) { productService.changeStatus(id, ProductStatus.INVALID); return redirect:/products/ id; } PostMapping(/{id}/delete) public String delete(PathVariable Long id) { productService.softDelete(id); return redirect:/products; } private String buildAttributeText(Product product) { if (product.getAttributes() null) { return ; } return product.getAttributes().stream() .map(a - a.getAttrName() a.getAttrValue()) .collect(Collectors.joining(;)); } }Controller 中把状态操作独立成具体的onSale、offSale、invalidate方法而没有让页面直接传递任意状态值。这样等于把状态流转限制在业务允许的范围里避免前端传一个不存在的状态或直接绕过业务规则。4.7 商品列表页面在src/main/resources/templates/product/list.html中编写列表页!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title商品列表/title style body { font-family: Microsoft YaHei, sans-serif; margin: 40px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background: #f5f5f5; } .tag { background: #e8f5e9; color: #2e7d32; padding: 2px 8px; border-radius: 4px; font-size: 12px; } a { margin-right: 8px; } /style /head body h2商品列表/h2 a th:href{/products/new}新增商品/a table thead tr thID/th th商品名称/th th品牌/th th分类/th th风格/th th价格/th th库存/th th状态/th th操作/th /tr /thead tbody tr th:eachproduct : ${products} td th:text${product.id}1/td td th:text${product.name}商品名称/td td th:text${product.brand}品牌/td td th:text${product.category}分类/td td th:text${product.style}风格/td td th:text${product.price}价格/td td th:text${product.stock}库存/td td span classtag th:text${product.status.desc}状态/span /td td a th:href{/products/{id}(id${product.id})}详情/a a th:href{/products/{id}/edit(id${product.id})}编辑/a form th:if${product.status.name() OFF_SALE} th:action{/products/{id}/onSale(id${product.id})} methodpost styledisplay:inline button typesubmit上架/button /form form th:if${product.status.name() ON_SALE} th:action{/products/{id}/offSale(id${product.id})} methodpost styledisplay:inline button typesubmit下架/button /form form th:if${product.status.name() ! INVALID} th:action{/products/{id}/invalidate(id${product.id})} methodpost styledisplay:inline button typesubmit标记失效/button /form form th:action{/products/{id}/delete(id${product.id})} methodpost styledisplay:inline button typesubmit删除/button /form /td /tr /tbody /table /body /html页面状态展示使用product.status.desc也就是枚举中的中文描述。操作按钮根据当前状态做条件渲染比如已上架时只显示下架按钮下架时只显示上架按钮已失效时不再显示上下架按钮只保留编辑、删除和详情入口。4.8 商品新增与编辑页面新增和编辑可以共用同一个模板form.html根据product.id是否为空判断是新增还是编辑!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title商品编辑/title style body { font-family: Microsoft YaHei, sans-serif; margin: 40px; } .form-item { margin-bottom: 12px; } label { display: inline-block; width: 100px; } input, textarea, select { width: 300px; padding: 6px; } textarea { height: 80px; vertical-align: top; } .error { color: red; } /style /head body h2 th:text${product.id null} ? 新增商品 : 编辑商品商品编辑/h2 form th:action${product.id null} ? {/products} : {/products/{id}/update(id${product.id})} th:object${product} methodpost div classform-item label商品名称/label input typetext th:field*{name} placeholder例如可拆洗布艺沙发床/ /div div classform-item label品牌/label input typetext th:field*{brand} placeholder例如狄普/ /div div classform-item label型号/label input typetext th:field*{model} placeholder例如雾屿 2026新款/ /div div classform-item label分类/label input typetext th:field*{category} placeholder例如沙发床/ /div div classform-item label风格/label input typetext th:field*{style} placeholder例如奶油风/ /div div classform-item label尺寸/label input typetext th:field*{size} placeholder例如1.55米/ /div div classform-item label价格/label input typenumber step0.01 th:field*{price}/ /div div classform-item label库存/label input typenumber th:field*{stock}/ /div div classform-item label状态/label select th:field*{status} option th:eachstatus : ${statusList} th:value${status.name()} th:text${status.desc}状态/option /select /div div classform-item label扩展属性/label textarea nameattributeText th:text${attributeText} placeholder格式材质雪尼尔;填充物羽绒;环保等级母婴A类/textarea /div div classform-item label描述/label textarea th:field*{description} placeholder商品详细描述/textarea /div button typesubmit保存/button a th:href{/products}返回列表/a /form /body /html扩展属性没有绑定到Product对象而是以独立的文本参数传递这样页面不需要维护复杂的动态列表组件。用户按“属性值;属性值”的格式填写后端解析后自动转换为结构化数据。4.9 商品详情页面详情页主要用于回显完整信息和操作当前商品!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title商品详情/title style body { font-family: Microsoft YaHei, sans-serif; margin: 40px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background: #f5f5f5; } /style /head body h2 th:text${product.name}商品名称/h2 table trth品牌/thtd th:text${product.brand}品牌/td/tr trth型号/thtd th:text${product.model}型号/td/tr trth分类/thtd th:text${product.category}分类/td/tr trth风格/thtd th:text${product.style}风格/td/tr trth尺寸/thtd th:text${product.size}尺寸/td/tr trth价格/thtd th:text${product.price}价格/td/tr trth库存/thtd th:text${product.stock}库存/td/tr trth状态/thtd th:text${product.status.desc}状态/td/tr trth描述/thtd th:text${product.description}描述/td/tr /table h3扩展属性/h3 table thead tr th属性名称/th th属性值/th /tr /thead tbody tr th:eachattr : ${product.attributes} td th:text${attr.attrName}属性名/td td th:text${attr.attrValue}属性值/td /tr /tbody /table p a th:href{/products/{id}/edit(id${product.id})}编辑/a a th:href{/products}返回列表/a /p /body /html4.10 启动运行与验证在项目根目录执行mvn spring-boot:run启动完成后访问http://localhost:8080/products页面会显示商品列表此时列表为空可以先点击“新增商品”录入一条数据。建议录入以下演示数据商品名称狄普雾屿 可拆洗布艺沙发床品牌狄普型号雾屿 2026新款分类沙发床风格奶油风尺寸1.55米价格3299库存50状态上架扩展属性风格奶油风;尺寸1.55米;面料雪尼尔;填充物羽绒;环保等级母婴A类;甲醛含量0;是否可拆洗可拆洗描述采用雪尼尔布艺面料羽绒填充达到母婴级环保标准。保存后会自动跳转到详情页可以看到扩展属性已经被拆分并逐条展示。回到列表页点击“下架”、“标记失效”可以观察状态变化。点击“删除”后商品会从列表消失但 H2 数据库中记录仍存在只被标记为deletedtrue。如果想要查看数据库中的真实状态可以访问 H2 控制台http://localhost:8080/h2-consoleJDBC URL 填写jdbc:h2:mem:testdb用户名sa密码留空登录后执行SELECT id, name, status, deleted, create_time, update_time FROM product;可以直观看到业务删除和物理删除的区别。5. 常见问题与排查思路在实际开发中把代码复制到本地后大概率会遇到几个问题这里整理成一张排查表。问题现象常见原因解决思路启动报“Table already exists”之前使用ddl-auto: update创建过旧表字段结构变化开发环境可以删除 H2 内存库或切换数据库文件路径重新启动保存商品后属性列表为空页面没有把attributeText提交到后端或属性文本格式不对检查表单中是否有nameattributeText并按“属性值;属性值”填写商品状态修改后不生效Service 方法没有加Transactional或前端请求没有正确匹配检查方法和路径确保PostMapping地址正确删除商品后列表仍然显示删除接口没有执行或查询方法没有加DeletedFalse条件检查删除操作是否调用了softDelete确认列表查询方法带过滤条件H2 控制台连接报“Database may be already in use”内存数据库被多个连接关闭导致数据丢失JDBC URL 加上DB_CLOSE_DELAY-1重启应用后再连接页面中文显示乱码文件编码
返回列表