jinshihui 5 дней назад
Родитель
Сommit
6eed2d9aa8
34 измененных файлов с 2024 добавлено и 1 удалено
  1. 233 0
      nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/ProductController.java
  2. 26 0
      nightFragrance-framework/src/main/java/com/ylx/framework/config/MyMetaObjectHandler.java
  3. 1 1
      nightFragrance-framework/src/main/java/com/ylx/framework/config/SecurityConfig.java
  4. 164 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/Product.java
  5. 80 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductCategory.java
  6. 63 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductImage.java
  7. 113 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductSku.java
  8. 54 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductSpec.java
  9. 63 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductSpecValue.java
  10. 132 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductCreateDTO.java
  11. 65 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSkuDTO.java
  12. 39 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSpecDTO.java
  13. 88 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSpecSetupDTO.java
  14. 32 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSpecValueDTO.java
  15. 56 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/CategoryTreeVO.java
  16. 86 0
      nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/SpecComboVO.java
  17. 33 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductCategoryMapper.java
  18. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductImageMapper.java
  19. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductMapper.java
  20. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSkuMapper.java
  21. 15 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSpecMapper.java
  22. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSpecValueMapper.java
  23. 24 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductCategoryService.java
  24. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductImageService.java
  25. 52 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductService.java
  26. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductSkuService.java
  27. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductSpecService.java
  28. 14 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductSpecValueService.java
  29. 30 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductCategoryServiceImpl.java
  30. 18 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductImageServiceImpl.java
  31. 391 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductServiceImpl.java
  32. 18 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductSkuServiceImpl.java
  33. 18 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductSpecServiceImpl.java
  34. 18 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductSpecValueServiceImpl.java

+ 233 - 0
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/ProductController.java

@@ -0,0 +1,233 @@
+package com.ylx.web.controller.massage;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.common.annotation.Log;
+import com.ylx.common.core.controller.BaseController;
+import com.ylx.common.core.domain.R;
+import com.ylx.common.enums.BusinessType;
+import com.ylx.common.utils.StringUtils;
+import com.ylx.massage.domain.Product;
+import com.ylx.massage.domain.ProductCategory;
+import com.ylx.massage.domain.dto.ProductCreateDTO;
+import com.ylx.massage.domain.dto.ProductSpecSetupDTO;
+import com.ylx.massage.domain.vo.CategoryTreeVO;
+import com.ylx.massage.domain.vo.SpecComboVO;
+import com.ylx.massage.service.ProductCategoryService;
+import com.ylx.massage.service.ProductService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 商品管理(Product)表控制层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Slf4j
+@Api(tags = {"商品管理"})
+@RestController
+@RequestMapping("product")
+public class ProductController extends BaseController {
+
+    @Resource
+    private ProductService productService;
+
+    @Resource
+    private ProductCategoryService productCategoryService;
+
+    /**
+     * 分页查询商品列表
+     *
+     * @param page    分页对象
+     * @param product 查询实体
+     * @return 所有数据
+     */
+    @RequestMapping(value = "/list", method = RequestMethod.GET)
+    @ApiOperation("PC查询商品管理列表")
+    public R<Page<Product>> selectAll(Page<Product> page, Product product) {
+        LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(product.getCategoryId() != null, Product::getCategoryId, product.getCategoryId())
+                .like(StringUtils.isNotBlank(product.getName()), Product::getName, product.getName())
+                .eq(product.getStatus() != null, Product::getStatus, product.getStatus())
+                .eq(product.getHasSpec() != null, Product::getHasSpec, product.getHasSpec())
+                .orderByDesc(Product::getCreatedAt);
+        return R.ok(this.productService.page(page, queryWrapper));
+    }
+
+    /**
+     * 通过主键查询单条数据
+     *
+     * @param id 主键
+     * @return 单条数据
+     */
+    @GetMapping("getById")
+    @ApiOperation("通过主键查询单条数据")
+    public R selectOne(Long id) {
+        return R.ok(this.productService.getById(id));
+    }
+
+    /**
+     * 新增商品
+     *
+     * @param dto 新增商品请求DTO
+     * @return R 新增结果
+     */
+    @PostMapping("create")
+    @Log(title = "商品管理新增数据", businessType = BusinessType.INSERT)
+    @ApiOperation("新增商品")
+    public R create(@Valid @RequestBody ProductCreateDTO dto) {
+        try {
+            Long productId = productService.createProduct(dto);
+            return R.ok(productId, "商品创建成功");
+        } catch (Exception e) {
+            log.error("商品创建失败", e);
+            return R.fail("商品创建失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 修改数据
+     *
+     * @param product 实体对象
+     * @return 修改结果
+     */
+    @PostMapping("update")
+    @Log(title = "商品管理修改数据", businessType = BusinessType.UPDATE)
+    @ApiOperation("修改数据")
+    public R update(@RequestBody Product product) {
+        return R.ok(this.productService.updateById(product));
+    }
+
+    /**
+     * 删除数据
+     *
+     * @param idList 主键结合
+     * @return 删除结果
+     */
+    @PostMapping("delete")
+    @ApiOperation("删除数据")
+    public R delete(@RequestBody List<Long> idList) {
+        return R.ok(this.productService.removeByIds(idList));
+    }
+
+    /**
+     * 上下架商品
+     *
+     * @param id     商品ID
+     * @param status 状态:1上架 0下架
+     * @return 操作结果
+     */
+    @PostMapping("updateStatus")
+    @Log(title = "商品管理上下架", businessType = BusinessType.UPDATE)
+    @ApiOperation("上下架商品")
+    public R updateStatus(@RequestParam("id") Long id, @RequestParam("status") Integer status) {
+        Product product = new Product();
+        product.setId(id);
+        product.setStatus(status);
+        return R.ok(this.productService.updateById(product));
+    }
+
+    /**
+     * 查询商品分类树形结构
+     *
+     * @return R<List<CategoryTreeVO>> 分类树形列表
+     */
+    @GetMapping("category/list")
+    @ApiOperation("查询商品分类树形结构")
+    public R<List<CategoryTreeVO>> categoryList() {
+        return R.ok(productCategoryService.getCategoryTree());
+    }
+
+    /**
+     * 新增商品分类
+     *
+     * @param category 分类实体
+     * @return R 新增结果
+     */
+    @PostMapping("category/create")
+    @ApiOperation("新增商品分类")
+    public R createCategory(@RequestBody ProductCategory category) {
+        return R.ok(productCategoryService.save(category));
+    }
+
+    /**
+     * 修改商品分类
+     *
+     * @param category 分类实体
+     * @return 修改结果
+     */
+    @PostMapping("category/update")
+    @Log(title = "商品分类修改", businessType = BusinessType.UPDATE)
+    @ApiOperation("修改商品分类")
+    public R updateCategory(@RequestBody ProductCategory category) {
+        return R.ok(productCategoryService.updateById(category));
+    }
+
+    /**
+     * 删除商品分类
+     *
+     * @param id 分类ID
+     * @return 删除结果
+     */
+    @PostMapping("category/delete")
+    @ApiOperation("删除商品分类")
+    public R deleteCategory(@RequestParam("id") Long id) {
+        return R.ok(productCategoryService.removeById(id));
+    }
+
+    /**
+     * 新增商品规格
+     *
+     * @param dto 规格设置请求DTO
+     * @return R 操作结果
+     */
+    @PostMapping("spec/setup")
+    @ApiOperation("设置商品规格")
+    public R setupSpec(@Valid @RequestBody ProductSpecSetupDTO dto) {
+        try {
+            productService.setupSpec(dto);
+            return R.ok("规格设置成功");
+        } catch (Exception e) {
+            log.error("规格设置失败", e);
+            return R.fail("规格设置失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取商品规格组合(笛卡尔积)
+     *
+     * @param productId 商品ID
+     * @return R<List<SpecComboVO>> 规格组合列表
+     */
+    @GetMapping("spec/combinations")
+    @ApiOperation("获取商品规格组合")
+    public R<List<SpecComboVO>> getSpecCombinations() {
+        return R.ok(productService.getSpecCombinations());
+    }
+
+    /**
+     * 生成商品编号
+     * 规则:分类编码 + 当前日期(yyyyMMdd) + 序列号(3位)
+     * 示例:AD20260327001
+     *
+     * @param categoryId 分类ID
+     * @return R<String> 生成的商品编号
+     */
+    @GetMapping("generateProductNo")
+    @ApiOperation("生成商品编号")
+    public R<String> generateProductNo(@RequestParam("categoryId") Long categoryId) {
+        try {
+            String productNo = productService.generateProductNo(categoryId);
+            return R.ok(productNo, "商品编号生成成功");
+        } catch (Exception e) {
+            log.error("商品编号生成失败", e);
+            return R.fail("商品编号生成失败:" + e.getMessage());
+        }
+    }
+}

+ 26 - 0
nightFragrance-framework/src/main/java/com/ylx/framework/config/MyMetaObjectHandler.java

@@ -0,0 +1,26 @@
+package com.ylx.framework.config;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Component
+@Slf4j
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        log.info("开始插入填充");
+        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
+        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        log.info("开始更新填充");
+        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
+    }
+}

+ 1 - 1
nightFragrance-framework/src/main/java/com/ylx/framework/config/SecurityConfig.java

@@ -115,7 +115,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/api/lbt/v1/getAll", "/api/js/v1/select", "/api/xiangmu/v1/wx/getAll", "/api/order/v1/getStatus",
                         "/api/xiangmu/v1/getByid", "/api/xiangmu/v1/highlights","/api/js/v1/wx/getByid","/api/js/v1/wx/select","/api/js/v1/wx/add", "/api/recharge/v1/test",
                         "/wx/pay/payNotify","/wx/pay/refundNotify","/weChat/getAccessToken","/weChat/getCode","/weChat/verifyToken","/sq/getAccessToken",
-                        "/area/select","/system/dept/list","/api/xiangmu/v1/wx/recommend").permitAll()
+                        "/area/select","/system/dept/list","/api/xiangmu/v1/wx/recommend","/product/category/create").permitAll()
                 // 静态资源,可匿名访问
                 .antMatchers(HttpMethod.GET, "/", "/*.txt","/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                 .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

+ 164 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/Product.java

@@ -0,0 +1,164 @@
+package com.ylx.massage.domain;
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+/**
+ * 商品表 实体类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName(value = "product",autoResultMap = true)
+public class Product implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 商品编号(如AD2026227001)
+     */
+    @TableField("product_no")
+    private String productNo;
+
+    /**
+     * 分类ID
+     */
+    @TableField("category_id")
+    private Long categoryId;
+
+    /**
+     * 商品名称
+     */
+    @TableField("name")
+    private String name;
+
+    /**
+     * 商品主图URL
+     */
+    @TableField("product_main_image")
+    private String productMainImage;
+
+    /**
+     * 商品图URL
+     */
+    @TableField(value = "product_image", typeHandler = JacksonTypeHandler.class)
+    private List<String> productImage;
+
+    /**
+     * 商品简述
+     */
+    @TableField("summary")
+    private String summary;
+
+    /**
+     * 商品详情(富文本HTML)
+     */
+    @TableField("detail")
+    private String detail;
+
+    /**
+     * 积分价格
+     */
+    @TableField("price_point")
+    private Integer pricePoint;
+
+    /**
+     * 现金价格(元)
+     */
+    @TableField("price_money")
+    private BigDecimal priceMoney;
+
+    /**
+     * 总库存(冗余,由SKU汇总)
+     */
+    @TableField("stock")
+    private Integer stock;
+
+    /**
+     * 已售数量
+     */
+    @TableField("sales")
+    private Integer sales;
+
+    /**
+     * 基础运费
+     */
+    @TableField("freight")
+    private BigDecimal freight;
+
+    /**
+     * 承诺发货时间(如48小时)
+     */
+    @TableField("delivery_time")
+    private String deliveryTime;
+
+    /**
+     * 服务承诺,JSON数组存储
+     * 例如:["假一赔三", "7天无理由退换", "破损包退换"]
+     */
+    @TableField(value = "service_promise", typeHandler = JacksonTypeHandler.class)
+    private List<String> servicePromise;
+
+    /**
+     * 上架开始时间
+     */
+    @TableField("sale_start_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime saleStartTime;
+
+    /**
+     * 上架结束时间
+     */
+    @TableField("sale_end_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime saleEndTime;
+
+    /**
+     * 状态:1上架 0下架
+     */
+    @TableField("status")
+    private Integer status;
+
+    /**
+     * 是否有规格:1是 0否
+     */
+    @TableField("has_spec")
+    private Integer hasSpec;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_at", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createdAt;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updatedAt;
+
+    /**
+     * 逻辑删除:1已删除 0未删除
+     */
+    @TableField("deleted")
+    @TableLogic
+    private Integer deleted;
+
+}

+ 80 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductCategory.java

@@ -0,0 +1,80 @@
+package com.ylx.massage.domain;
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 商品分类表 实体类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("product_category")
+public class ProductCategory implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 父分类ID
+     */
+    @TableField("parent_id")
+    private Long parentId;
+
+    /**
+     * 分类编码
+     */
+    @TableField("code")
+    private String code;
+
+    /**
+     * 分类名称(休闲/日用/娱乐)
+     */
+    @TableField("name")
+    private String name;
+
+    /**
+     * 分类级别
+     */
+    @TableField("level")
+    private Integer level;
+
+    /**
+     * 排序
+     */
+    @TableField("sort")
+    private Integer sort;
+
+    /**
+     * 状态:1启用 0禁用
+     */
+    @TableField("status")
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+
+}

+ 63 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductImage.java

@@ -0,0 +1,63 @@
+package com.ylx.massage.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 商品图片表 实体类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("product_image")
+public class ProductImage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 商品ID
+     */
+    @TableField("product_id")
+    private Long productId;
+
+    /**
+     * 图片URL
+     */
+    @TableField("url")
+    private String url;
+
+    /**
+     * 类型:1主图 2副图
+     */
+    @TableField("type")
+    private Integer type;
+
+    /**
+     * 排序
+     */
+    @TableField("sort")
+    private Integer sort;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_at", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createdAt;
+
+}

+ 113 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductSku.java

@@ -0,0 +1,113 @@
+package com.ylx.massage.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 商品SKU表 实体类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("product_sku")
+public class ProductSku implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 商品ID
+     */
+    @TableField("product_id")
+    private Long productId;
+
+    /**
+     * SKU编号
+     */
+    @TableField("sku_no")
+    private String skuNo;
+
+    /**
+     * 规格组合(如:白色,加粗,大号)
+     */
+    @TableField("spec_combo")
+    private String specCombo;
+
+    /**
+     * 规格值ID组合(如:1_3_5)
+     */
+    @TableField("spec_value_ids")
+    private String specValueIds;
+
+    /**
+     * SKU图片URL
+     */
+    @TableField("image")
+    private String image;
+
+    /**
+     * 现金价格
+     */
+    @TableField("price_money")
+    private BigDecimal priceMoney;
+
+    /**
+     * 积分价格
+     */
+    @TableField("price_point")
+    private Integer pricePoint;
+
+    /**
+     * 原价
+     */
+    @TableField("origin_price")
+    private BigDecimal originPrice;
+
+    /**
+     * 库存
+     */
+    @TableField("stock")
+    private Integer stock;
+
+    /**
+     * 已售
+     */
+    @TableField("sales")
+    private Integer sales;
+
+    /**
+     * 状态:1启用 0禁用
+     */
+    @TableField("status")
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_at", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createdAt;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updatedAt;
+
+}

+ 54 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductSpec.java

@@ -0,0 +1,54 @@
+package com.ylx.massage.domain;
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+/**
+ * 商品规格表 实体类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("product_spec")
+public class ProductSpec implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 商品ID
+     */
+    @TableField("product_id")
+    private Long productId;
+
+    /**
+     * 规格名(颜色/型号/尺码)
+     */
+    @TableField("spec_name")
+    private String specName;
+
+    /**
+     * 排序
+     */
+    @TableField("sort")
+    private Integer sort;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+}

+ 63 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/ProductSpecValue.java

@@ -0,0 +1,63 @@
+package com.ylx.massage.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 商品规格值表 实体类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("product_spec_value")
+public class ProductSpecValue implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 规格名ID
+     */
+    @TableField("spec_id")
+    private Long specId;
+
+    /**
+     * 商品ID(冗余,便于查询)
+     */
+    @TableField("product_id")
+    private Long productId;
+
+    /**
+     * 规格值(白色/红色/加粗)
+     */
+    @TableField("spec_value")
+    private String specValue;
+
+    /**
+     * 排序
+     */
+    @TableField("sort")
+    private Integer sort;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+}

+ 132 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductCreateDTO.java

@@ -0,0 +1,132 @@
+package com.ylx.massage.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 新增商品请求DTO
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@ApiModel("新增商品请求")
+public class ProductCreateDTO {
+
+    /**
+     * 分类ID
+     */
+    @ApiModelProperty(value = "分类ID", required = true)
+    @NotNull(message = "分类ID不能为空")
+    private Long categoryId;
+
+    /**
+     * 商品编号
+     */
+    @ApiModelProperty(value = "商品编号", required = true)
+    @NotBlank(message = "商品编号不能为空")
+    private String productNo;
+
+    /**
+     * 商品名称
+     */
+    @ApiModelProperty(value = "商品名称", required = true)
+    @NotBlank(message = "商品名称不能为空")
+    private String name;
+
+    /**
+     * 商品主图URL
+     */
+    @ApiModelProperty(value = "商品主图URL", required = true)
+    @NotBlank(message = "商品主图不能为空")
+    private String productMainImage;
+
+    /**
+     * 商品图URL列表
+     */
+    @ApiModelProperty("商品图URL列表")
+    private List<String> productImage;
+
+    /**
+     * 商品简述
+     */
+    @ApiModelProperty("商品简述")
+    private String summary;
+
+    /**
+     * 商品详情(富文本HTML)
+     */
+    @ApiModelProperty("商品详情")
+    private String detail;
+
+    /**
+     * 规格列表
+     */
+    @ApiModelProperty("规格列表")
+    @Valid
+    private List<ProductSpecDTO> specList;
+
+    /**
+     * SKU列表(规格组合明细)
+     */
+    @ApiModelProperty("SKU列表")
+    @Valid
+    private List<ProductSkuDTO> skuList;
+
+    /**
+     * 积分价格(无规格时使用)
+     */
+    @ApiModelProperty("积分价格")
+    private Integer pricePoint;
+
+    /**
+     * 现金价格(无规格时使用)
+     */
+    @ApiModelProperty("现金价格")
+    private BigDecimal priceMoney;
+
+    /**
+     * 库存(无规格时使用)
+     */
+    @ApiModelProperty("库存")
+    private Integer stock;
+
+    /**
+     * 基础运费
+     */
+    @ApiModelProperty("基础运费")
+    private BigDecimal freight;
+
+    /**
+     * 承诺发货时间
+     */
+    @ApiModelProperty("承诺发货时间(如48小时)")
+    private String deliveryTime;
+
+    /**
+     * 服务承诺列表
+     * 例如:["假一赔三", "7天无理由退换", "破损包退换"]
+     */
+    @ApiModelProperty("服务承诺列表")
+    private List<String> servicePromise;
+
+    /**
+     * 上架开始时间
+     */
+    @ApiModelProperty("上架开始时间")
+    private LocalDateTime saleStartTime;
+
+    /**
+     * 上架结束时间
+     */
+    @ApiModelProperty("上架结束时间")
+    private LocalDateTime saleEndTime;
+}

+ 65 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSkuDTO.java

@@ -0,0 +1,65 @@
+package com.ylx.massage.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * 商品SKU DTO(规格组合明细)
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@ApiModel("商品SKU")
+public class ProductSkuDTO {
+
+    /**
+     * 规格组合(如:白色,加粗,大号)
+     */
+    @ApiModelProperty(value = "规格组合", required = true)
+    private String specCombo;
+
+    /**
+     * 规格值ID组合(如:1_3_5,前端传递规格值的索引组合)
+     */
+    @ApiModelProperty(value = "规格值索引组合(如:0_1_2)", required = true)
+    private String specValueIndexCombo;
+
+    /**
+     * SKU图片URL
+     */
+    @ApiModelProperty("SKU图片URL")
+    private String image;
+
+    /**
+     * 现金价格
+     */
+    @ApiModelProperty(value = "现金价格", required = true)
+    @NotNull(message = "现金价格不能为空")
+    private BigDecimal priceMoney;
+
+    /**
+     * 积分价格
+     */
+    @ApiModelProperty(value = "积分价格", required = true)
+    @NotNull(message = "积分价格不能为空")
+    private Integer pricePoint;
+
+    /**
+     * 原价
+     */
+    @ApiModelProperty("原价")
+    private BigDecimal originPrice;
+
+    /**
+     * 库存
+     */
+    @ApiModelProperty(value = "库存", required = true)
+    @NotNull(message = "库存不能为空")
+    private Integer stock;
+
+}

+ 39 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSpecDTO.java

@@ -0,0 +1,39 @@
+package com.ylx.massage.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.util.List;
+
+/**
+ * 商品规格DTO
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@ApiModel("商品规格")
+public class ProductSpecDTO {
+
+    /**
+     * 规格名(颜色/型号/尺码)
+     */
+    @ApiModelProperty(value = "规格名", required = true)
+    @NotBlank(message = "规格名不能为空")
+    private String specName;
+
+    /**
+     * 排序
+     */
+    @ApiModelProperty("排序")
+    private Integer sort;
+
+    /**
+     * 规格值列表
+     */
+    @ApiModelProperty(value = "规格值列表", required = true)
+    private List<ProductSpecValueDTO> specValues;
+
+}

+ 88 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSpecSetupDTO.java

@@ -0,0 +1,88 @@
+package com.ylx.massage.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 商品规格设置请求DTO
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@ApiModel("商品规格设置请求")
+public class ProductSpecSetupDTO {
+
+    /**
+     * 规格列表
+     */
+    @ApiModelProperty(value = "规格列表", required = true)
+    @Valid
+    private List<SpecItemDTO> specList;
+
+    /**
+     * 规格项
+     */
+    @Data
+    @ApiModel("规格项")
+    public static class SpecItemDTO {
+
+        /**
+         * 规格名ID(编辑时传入,新增时不传)
+         */
+        @ApiModelProperty("规格名ID(编辑时传入)")
+        private Long specId;
+
+        /**
+         * 规格名(如:颜色、型号、尺码)
+         */
+        @ApiModelProperty(value = "规格名", required = true)
+        @NotNull(message = "规格名不能为空")
+        private String specName;
+
+        /**
+         * 排序
+         */
+        @ApiModelProperty("排序")
+        private Integer sort;
+
+        /**
+         * 规格值列表
+         */
+        @ApiModelProperty(value = "规格值列表", required = true)
+        @Valid
+        private List<SpecValueItemDTO> specValues;
+    }
+
+    /**
+     * 规格值项
+     */
+    @Data
+    @ApiModel("规格值项")
+    public static class SpecValueItemDTO {
+
+        /**
+         * 规格值ID(编辑时传入,新增时不传)
+         */
+        @ApiModelProperty("规格值ID(编辑时传入)")
+        private Long specValueId;
+
+        /**
+         * 规格值(如:白色、红色、加粗、大号)
+         */
+        @ApiModelProperty(value = "规格值", required = true)
+        @NotNull(message = "规格值不能为空")
+        private String specValue;
+
+        /**
+         * 排序
+         */
+        @ApiModelProperty("排序")
+        private Integer sort;
+    }
+}

+ 32 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/dto/ProductSpecValueDTO.java

@@ -0,0 +1,32 @@
+package com.ylx.massage.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 商品规格值DTO
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@ApiModel("商品规格值")
+public class ProductSpecValueDTO {
+
+    /**
+     * 规格值(白色/红色/加粗)
+     */
+    @ApiModelProperty(value = "规格值", required = true)
+    @NotBlank(message = "规格值不能为空")
+    private String specValue;
+
+    /**
+     * 排序
+     */
+    @ApiModelProperty("排序")
+    private Integer sort;
+
+}

+ 56 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/CategoryTreeVO.java

@@ -0,0 +1,56 @@
+package com.ylx.massage.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商品分类树形结构VO
+ *
+ * @author ylx
+ * @since 2026-03-27
+ */
+@Data
+@ApiModel("商品分类树形结构")
+public class CategoryTreeVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 父分类ID
+     */
+    @ApiModelProperty("父分类ID")
+    private Long parentId;
+
+    /**
+     * 父分类名称
+     */
+    @ApiModelProperty("父分类名称")
+    private String parentName;
+
+    /**
+     * 子分类ID
+     */
+    @ApiModelProperty("子分类ID")
+    private Long childId;
+
+    /**
+     * 子分类编码
+     */
+    @ApiModelProperty("子分类编码")
+    private String childCode;
+
+    /**
+     * 子分类名称
+     */
+    @ApiModelProperty("子分类名称")
+    private String childName;
+
+    /**
+     * 子分类排序
+     */
+    @ApiModelProperty("子分类排序")
+    private Integer childSort;
+}

+ 86 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/SpecComboVO.java

@@ -0,0 +1,86 @@
+package com.ylx.massage.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 规格组合VO(笛卡尔积结果)
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Data
+@ApiModel("规格组合")
+public class SpecComboVO {
+
+    /**
+     * 组合索引(用于前端标识)
+     */
+    @ApiModelProperty("组合索引")
+    private Integer index;
+
+    /**
+     * 规格值组合(如:["白色", "加粗"])
+     */
+    @ApiModelProperty("规格值组合")
+    private List<String> specValues;
+
+    /**
+     * 规格值ID组合(如:[1, 3])
+     */
+    /*@ApiModelProperty("规格值ID组合")
+    private List<Long> specValueIds;*/
+
+    /**
+     * 组合显示文本(如:"白色,加粗")
+     */
+    @ApiModelProperty("组合显示文本")
+    private String specValueText;
+
+    /**
+     * 规格名对应关系(如:[{"specName":"颜色","specValue":"白色"},{"specName":"型号","specValue":"加粗"}])
+     */
+    @ApiModelProperty("规格名对应关系")
+    private List<SpecNameValue> specNameValueList;
+
+    /**
+     * 规格名值对
+     */
+    @Data
+    @ApiModel("规格名值对")
+    public static class SpecNameValue {
+
+        /**
+         * 规格名ID
+         */
+        @ApiModelProperty("规格名ID")
+        private Long specId;
+
+        /**
+         * 规格名
+         */
+        @ApiModelProperty("规格名")
+        private String specName;
+
+        /**
+         * 规格值ID
+         */
+        /*@ApiModelProperty("规格值ID")
+        private Long specValueId;*/
+
+        /**
+         * 规格值
+         */
+        @ApiModelProperty("规格值")
+        private String specValue;
+
+        /**
+         * 排序
+         */
+        @ApiModelProperty("排序")
+        private Integer sort;
+    }
+}

+ 33 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductCategoryMapper.java

@@ -0,0 +1,33 @@
+package com.ylx.massage.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.massage.domain.ProductCategory;
+import com.ylx.massage.domain.vo.CategoryTreeVO;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 商品分类表(ProductCategory)表数据库访问层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductCategoryMapper extends BaseMapper<ProductCategory> {
+
+    /**
+     * 查询分类树形结构(父子分类关联查询)
+     *
+     * @return List<CategoryTreeVO> 分类树形列表
+     */
+    @Select("SELECT " +
+            "    p.id  AS parent_id, " +
+            "    p.name  AS parent_name, " +
+            "    c.id  AS child_id, " +
+            "    c.code  AS child_code, " +
+            "    c.name  AS child_name, " +
+            "    c.sort  AS child_sort " +
+            "FROM product_category p LEFT JOIN product_category c ON c.parent_id = p.id WHERE p.parent_id = 0 and p.`status` = 1 ORDER BY p.sort, c.sort")
+    public List<CategoryTreeVO> selectCategoryTree();
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductImageMapper.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.massage.domain.ProductImage;
+
+/**
+ * 商品图片表(ProductImage)表数据库访问层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductImageMapper extends BaseMapper<ProductImage> {
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductMapper.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.massage.domain.Product;
+
+/**
+ * 商品表(Product)表数据库访问层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductMapper extends BaseMapper<Product> {
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSkuMapper.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.massage.domain.ProductSku;
+
+/**
+ * 商品SKU表(ProductSku)表数据库访问层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductSkuMapper extends BaseMapper<ProductSku> {
+
+}

+ 15 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSpecMapper.java

@@ -0,0 +1,15 @@
+package com.ylx.massage.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.massage.domain.ProductSpec;
+
+/**
+ * 商品规格表(ProductSpec)表数据库访问层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+
+public interface ProductSpecMapper extends BaseMapper<ProductSpec> {
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/ProductSpecValueMapper.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.massage.domain.ProductSpecValue;
+
+/**
+ * 商品规格值表(ProductSpecValue)表数据库访问层
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductSpecValueMapper extends BaseMapper<ProductSpecValue> {
+
+}

+ 24 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductCategoryService.java

@@ -0,0 +1,24 @@
+package com.ylx.massage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.massage.domain.ProductCategory;
+import com.ylx.massage.domain.vo.CategoryTreeVO;
+
+import java.util.List;
+
+/**
+ * 商品分类表(ProductCategory)表服务接口
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductCategoryService extends IService<ProductCategory> {
+
+    /**
+     * 查询分类树形结构(父子分类关联查询)
+     *
+     * @return 分类树形列表
+     */
+    List<CategoryTreeVO> getCategoryTree();
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductImageService.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.massage.domain.ProductImage;
+
+/**
+ * 商品图片表(ProductImage)表服务接口
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductImageService extends IService<ProductImage> {
+
+}

+ 52 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductService.java

@@ -0,0 +1,52 @@
+package com.ylx.massage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.massage.domain.Product;
+import com.ylx.massage.domain.dto.ProductCreateDTO;
+import com.ylx.massage.domain.dto.ProductSpecSetupDTO;
+import com.ylx.massage.domain.vo.SpecComboVO;
+
+import java.util.List;
+
+/**
+ * 商品表(Product)表服务接口
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductService extends IService<Product> {
+
+    /**
+     * 新增商品(包含规格、SKU、图片)
+     *
+     * @param dto 新增商品请求DTO
+     * @return 商品ID
+     */
+    Long createProduct(ProductCreateDTO dto);
+
+    /**
+     * 设置商品规格(新增或更新规格名和规格值)
+     *
+     * @param dto 规格设置请求DTO
+     */
+    void setupSpec(ProductSpecSetupDTO dto);
+
+    /**
+     * 获取商品规格组合(笛卡尔积)
+     *
+     * @param productId 商品ID
+     * @return 规格组合列表
+     */
+    List<SpecComboVO> getSpecCombinations();
+
+    /**
+     * 生成商品编号
+     * 规则:分类编码 + 当前日期(yyyyMMdd) + 序列号(3位)
+     * 示例:AD20260327001
+     *
+     * @param categoryId 分类ID
+     * @return 生成的商品编号
+     */
+    String generateProductNo(Long categoryId);
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductSkuService.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.massage.domain.ProductSku;
+
+/**
+ * 商品SKU表(ProductSku)表服务接口
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductSkuService extends IService<ProductSku> {
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductSpecService.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.massage.domain.ProductSpec;
+
+/**
+ * 商品规格表(ProductSpec)表服务接口
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductSpecService extends IService<ProductSpec> {
+
+}

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/ProductSpecValueService.java

@@ -0,0 +1,14 @@
+package com.ylx.massage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ylx.massage.domain.ProductSpecValue;
+
+/**
+ * 商品规格值表(ProductSpecValue)表服务接口
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+public interface ProductSpecValueService extends IService<ProductSpecValue> {
+
+}

+ 30 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductCategoryServiceImpl.java

@@ -0,0 +1,30 @@
+package com.ylx.massage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.massage.domain.ProductCategory;
+import com.ylx.massage.domain.vo.CategoryTreeVO;
+import com.ylx.massage.mapper.ProductCategoryMapper;
+import com.ylx.massage.service.ProductCategoryService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 商品分类表(ProductCategory)表服务实现类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Service("productCategoryService")
+public class ProductCategoryServiceImpl extends ServiceImpl<ProductCategoryMapper, ProductCategory> implements ProductCategoryService {
+
+    @Resource
+    private ProductCategoryMapper productCategoryMapper;
+
+    @Override
+    public List<CategoryTreeVO> getCategoryTree() {
+        return productCategoryMapper.selectCategoryTree();
+    }
+
+}

+ 18 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductImageServiceImpl.java

@@ -0,0 +1,18 @@
+package com.ylx.massage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.massage.domain.ProductImage;
+import com.ylx.massage.mapper.ProductImageMapper;
+import com.ylx.massage.service.ProductImageService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 商品图片表(ProductImage)表服务实现类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Service("productImageService")
+public class ProductImageServiceImpl extends ServiceImpl<ProductImageMapper, ProductImage> implements ProductImageService {
+
+}

+ 391 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductServiceImpl.java

@@ -0,0 +1,391 @@
+package com.ylx.massage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.massage.domain.*;
+import com.ylx.massage.domain.dto.ProductCreateDTO;
+import com.ylx.massage.domain.dto.ProductSkuDTO;
+import com.ylx.massage.domain.dto.ProductSpecDTO;
+import com.ylx.massage.domain.dto.ProductSpecSetupDTO;
+import com.ylx.massage.domain.dto.ProductSpecValueDTO;
+import com.ylx.massage.domain.vo.SpecComboVO;
+import com.ylx.massage.mapper.*;
+import com.ylx.massage.service.ProductService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * 商品表(Product)表服务实现类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Slf4j
+@Service("productService")
+public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
+
+    @Resource
+    private ProductCategoryMapper productCategoryMapper;
+
+    @Resource
+    private ProductImageMapper productImageMapper;
+
+    @Resource
+    private ProductSpecMapper productSpecMapper;
+
+    @Resource
+    private ProductSpecValueMapper productSpecValueMapper;
+
+    @Resource
+    private ProductSkuMapper productSkuMapper;
+
+    /**
+     * 新增商品(包含规格、SKU、图片)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createProduct(ProductCreateDTO dto) {
+        // 1. 校验分类是否存在
+        ProductCategory category = productCategoryMapper.selectById(dto.getCategoryId());
+        if (category == null) {
+            throw new RuntimeException("商品分类不存在");
+        }
+
+        // 2. 生成商品编号
+        String productNo = generateProductNo(dto.getCategoryId());
+
+        // 3. 保存商品基本信息
+        Product product = new Product();
+        product.setProductNo(productNo);
+        product.setCategoryId(dto.getCategoryId());
+        product.setName(dto.getName());
+        product.setProductMainImage(dto.getProductMainImage());
+        product.setProductImage(dto.getProductImage());
+        product.setSummary(dto.getSummary());
+        product.setDetail(dto.getDetail());
+        product.setPricePoint(dto.getPricePoint());
+        product.setPriceMoney(dto.getPriceMoney());
+        product.setStock(dto.getStock());
+        product.setFreight(dto.getFreight());
+        product.setDeliveryTime(dto.getDeliveryTime());
+        product.setServicePromise(dto.getServicePromise());
+        product.setSaleStartTime(dto.getSaleStartTime());
+        product.setSaleEndTime(dto.getSaleEndTime());
+        //product.setHasSpec(dto.getHasSpec());
+        product.setStatus(0); // 默认下架
+        product.setSales(0); // 初始销量为0
+        product.setDeleted(0); // 未删除
+
+        // 计算总库存
+        if (!CollectionUtils.isEmpty(dto.getSkuList())) {
+            int totalStock = dto.getSkuList().stream()
+                    .mapToInt(ProductSkuDTO::getStock)
+                    .sum();
+            product.setStock(totalStock);
+        }
+
+        this.save(product);
+        Long productId = product.getId();
+
+        // 4. 保存商品副图
+        saveProductImages(productId, dto.getProductImage());
+
+        // 5. 如果有规格,保存规格和SKU
+        if (!CollectionUtils.isEmpty(dto.getSpecList())) {
+            saveProductSpecsAndSkus(productId, dto);
+        }
+
+        log.info("商品创建成功,商品ID:{},商品编号:{}", productId, productNo);
+        return productId;
+    }
+
+    /**
+     * 生成商品编号(公开方法,供Controller调用)
+     * 规则:分类编码 + 当前日期(yyyyMMdd) + 序列号(3位)
+     * 示例:AD20260327001
+     *
+     * @param categoryId 分类ID
+     * @return 生成的商品编号
+     */
+    @Override
+    public String generateProductNo(Long categoryId) {
+        // 1. 根据分类ID获取分类信息
+        ProductCategory category = productCategoryMapper.selectById(categoryId);
+        if (category == null) {
+            throw new RuntimeException("商品分类不存在");
+        }
+
+        // 2. 获取分类编码
+        String categoryCode = category.getCode();
+        if (categoryCode == null || categoryCode.trim().isEmpty()) {
+            throw new RuntimeException("商品分类编码不存在");
+        }
+
+        // 3. 生成商品编号
+        return generateProductNoByCategoryCode(categoryCode);
+    }
+
+    /**
+     * 根据分类编码生成商品编号(私有方法)
+     * 规则:分类编码 + 当前日期(yyyyMMdd) + 序列号(3位)
+     *
+     * @param categoryCode 分类编码
+     * @return String 生成的商品编号
+     */
+    private String generateProductNoByCategoryCode(String categoryCode) {
+        String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        String prefix = categoryCode.toUpperCase();
+
+        // 查询当天该分类下最大编号
+        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
+        wrapper.likeRight(Product::getProductNo, prefix + dateStr).orderByDesc(Product::getProductNo).last("LIMIT 1");
+        Product lastProduct = this.getOne(wrapper);
+
+        int seq = 1;
+        if (lastProduct != null && lastProduct.getProductNo() != null) {
+            String lastNo = lastProduct.getProductNo();
+            // 从编号中提取序列号(最后3位)
+            String seqStr = lastNo.substring(lastNo.length() - 3);
+            try {
+                seq = Integer.parseInt(seqStr) + 1;
+            } catch (NumberFormatException e) {
+                log.warn("解析商品编号序列号失败:{}", lastNo);
+                seq = 1;
+            }
+        }
+        return prefix + dateStr + String.format("%03d", seq);
+    }
+
+    /**
+     * 保存商品图片
+     */
+    private void saveProductImages(Long productId, List<String> imageUrls) {
+        if (CollectionUtils.isEmpty(imageUrls)) {
+            return;
+        }
+
+        List<ProductImage> images = new ArrayList<>();
+        for (int i = 0; i < imageUrls.size(); i++) {
+            ProductImage image = new ProductImage();
+            image.setProductId(productId);
+            image.setUrl(imageUrls.get(i));
+            image.setType(2); // 副图
+            image.setSort(i + 1);
+            image.setCreatedAt(LocalDateTime.now());
+            images.add(image);
+        }
+        images.forEach(productImageMapper::insert);
+    }
+
+    /**
+     * 保存商品规格和SKU
+     */
+    private void saveProductSpecsAndSkus(Long productId, ProductCreateDTO dto) {
+        List<ProductSpecDTO> specList = dto.getSpecList();
+        List<ProductSkuDTO> skuList = dto.getSkuList();
+
+        if (CollectionUtils.isEmpty(specList) || CollectionUtils.isEmpty(skuList)) {
+            throw new RuntimeException("规格商品必须包含规格和SKU信息");
+        }
+
+        // 保存规格名和规格值,并记录ID映射
+        AtomicInteger specIndex = new AtomicInteger(0);
+
+        for (ProductSpecDTO specDTO : specList) {
+            // 保存规格名
+            ProductSpec spec = new ProductSpec();
+            spec.setProductId(productId);
+            spec.setSpecName(specDTO.getSpecName());
+            spec.setSort(specDTO.getSort() != null ? specDTO.getSort() : specIndex.get() + 1);
+            spec.setCreateTime(LocalDateTime.now());
+            productSpecMapper.insert(spec);
+            Long specId = spec.getId();
+
+            // 保存规格值
+            if (!CollectionUtils.isEmpty(specDTO.getSpecValues())) {
+                AtomicInteger valueIndex = new AtomicInteger(0);
+                for (ProductSpecValueDTO valueDTO : specDTO.getSpecValues()) {
+                    ProductSpecValue specValue = new ProductSpecValue();
+                    specValue.setSpecId(specId);
+                    specValue.setProductId(productId);
+                    specValue.setSpecValue(valueDTO.getSpecValue());
+                    specValue.setSort(valueDTO.getSort() != null ? valueDTO.getSort() : valueIndex.get() + 1);
+                    specValue.setCreateTime(LocalDateTime.now());
+                    productSpecValueMapper.insert(specValue);
+                    valueIndex.getAndIncrement();
+                }
+            }
+            specIndex.getAndIncrement();
+        }
+
+        // 保存SKU
+        for (ProductSkuDTO skuDTO : skuList) {
+            ProductSku sku = new ProductSku();
+            sku.setProductId(productId);
+            sku.setSkuNo(generateSkuNo(productId));
+            sku.setSpecCombo(skuDTO.getSpecCombo());
+            sku.setSpecValueIds(skuDTO.getSpecValueIndexCombo());
+            sku.setImage(skuDTO.getImage());
+            sku.setPriceMoney(skuDTO.getPriceMoney());
+            sku.setPricePoint(skuDTO.getPricePoint());
+            sku.setOriginPrice(skuDTO.getOriginPrice());
+            sku.setStock(skuDTO.getStock());
+            sku.setSales(0);
+            sku.setStatus(1);
+            sku.setCreatedAt(LocalDateTime.now());
+            sku.setUpdatedAt(LocalDateTime.now());
+            productSkuMapper.insert(sku);
+        }
+    }
+
+    /**
+     * 生成SKU编号
+     */
+    private String generateSkuNo(Long productId) {
+        return "SKU" + productId + System.currentTimeMillis();
+    }
+
+    /**
+     * 设置商品规格
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void setupSpec(ProductSpecSetupDTO dto) {
+        // 保存新的规格
+        if (!CollectionUtils.isEmpty(dto.getSpecList())) {
+            AtomicInteger specIndex = new AtomicInteger(0);
+            for (ProductSpecSetupDTO.SpecItemDTO specItem : dto.getSpecList()) {
+                // 保存规格名
+                ProductSpec spec = new ProductSpec();
+                //spec.setProductId(productId);
+                // 检查规格名是否存在,如果存在,则直接通过,不插入
+                LambdaQueryWrapper<ProductSpec> specWrapper = new LambdaQueryWrapper<>();
+                specWrapper.eq(ProductSpec::getSpecName, specItem.getSpecName());
+                ProductSpec existingSpec = productSpecMapper.selectOne(specWrapper);
+                if (existingSpec != null) {
+                    continue;
+                }
+                spec.setSpecName(specItem.getSpecName());
+                spec.setSort(specItem.getSort() != null ? specItem.getSort() : specIndex.get() + 1);
+                spec.setCreateTime(LocalDateTime.now());
+                productSpecMapper.insert(spec);
+                Long specId = spec.getId();
+
+                // 保存规格值
+                if (!CollectionUtils.isEmpty(specItem.getSpecValues())) {
+                    AtomicInteger valueIndex = new AtomicInteger(0);
+                    for (ProductSpecSetupDTO.SpecValueItemDTO valueItem : specItem.getSpecValues()) {
+                        ProductSpecValue specValue = new ProductSpecValue();
+                        specValue.setSpecId(specId);
+                        //specValue.setProductId(productId);
+                        specValue.setSpecValue(valueItem.getSpecValue());
+                        specValue.setSort(valueItem.getSort() != null ? valueItem.getSort() : valueIndex.get() + 1);
+                        specValue.setCreateTime(LocalDateTime.now());
+                        productSpecValueMapper.insert(specValue);
+                        valueIndex.getAndIncrement();
+                    }
+                }
+                specIndex.getAndIncrement();
+            }
+        }
+    }
+
+    /**
+     * 获取商品规格组合(笛卡尔积)
+     */
+    @Override
+    public List<SpecComboVO> getSpecCombinations() {
+        // 查询商品的所有规格名
+        LambdaQueryWrapper<ProductSpec> specWrapper = new LambdaQueryWrapper<>();
+        //specWrapper.eq(ProductSpec::getProductId, productId).orderByAsc(ProductSpec::getSort);
+        specWrapper.orderByAsc(ProductSpec::getSort);
+        List<ProductSpec> specList = productSpecMapper.selectList(specWrapper);
+
+        if (CollectionUtils.isEmpty(specList)) {
+            return Collections.emptyList();
+        }
+
+        // 查询所有规格值,按规格名分组
+        LambdaQueryWrapper<ProductSpecValue> valueWrapper = new LambdaQueryWrapper<>();
+        //valueWrapper.eq(ProductSpecValue::getProductId, productId).orderByAsc(ProductSpecValue::getSort);
+        valueWrapper.orderByAsc(ProductSpecValue::getSort);
+        List<ProductSpecValue> allValues = productSpecValueMapper.selectList(valueWrapper);
+
+        // 按规格名ID分组
+        Map<Long, List<ProductSpecValue>> valueMap = allValues.stream().collect(Collectors.groupingBy(ProductSpecValue::getSpecId));
+
+        // 构建每个规格的规格值列表
+        List<List<SpecComboVO.SpecNameValue>> specValueGroups = new ArrayList<>();
+        for (ProductSpec spec : specList) {
+            List<ProductSpecValue> values = valueMap.getOrDefault(spec.getId(), Collections.emptyList());
+            if (values.isEmpty()) {
+                continue;
+            }
+            List<SpecComboVO.SpecNameValue> nameValueList = new ArrayList<>();
+            for (ProductSpecValue value : values) {
+                SpecComboVO.SpecNameValue nameValue = new SpecComboVO.SpecNameValue();
+                nameValue.setSpecId(spec.getId());
+                nameValue.setSpecName(spec.getSpecName());
+                nameValue.setSpecValue(value.getSpecValue());
+                nameValue.setSort(value.getSort());
+                nameValueList.add(nameValue);
+            }
+            specValueGroups.add(nameValueList);
+        }
+
+        // 计算笛卡尔积
+        List<List<SpecComboVO.SpecNameValue>> cartesianProduct = cartesianProduct(specValueGroups);
+        // 构建返回结果
+        List<SpecComboVO> result = new ArrayList<>();
+        int index = 1;
+        for (List<SpecComboVO.SpecNameValue> combo : cartesianProduct) {
+            SpecComboVO vo = new SpecComboVO();
+            vo.setIndex(index++);
+
+            List<String> specValues = combo.stream().map(SpecComboVO.SpecNameValue::getSpecValue).collect(Collectors.toList());
+            vo.setSpecValues(specValues);
+
+            /*List<Long> specValueIds = combo.stream().map(SpecComboVO.SpecNameValue::getSpecValueId).collect(Collectors.toList());
+            vo.setSpecValueIds(specValueIds);*/
+
+            vo.setSpecValueText(String.join(",", specValues));
+            vo.setSpecNameValueList(combo);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 计算笛卡尔积
+     */
+    private <T> List<List<T>> cartesianProduct(List<List<T>> lists) {
+        if (lists.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<List<T>> result = new ArrayList<>();
+        result.add(new ArrayList<>());
+
+        for (List<T> list : lists) {
+            List<List<T>> temp = new ArrayList<>();
+            for (List<T> partial : result) {
+                for (T item : list) {
+                    List<T> newPartial = new ArrayList<>(partial);
+                    newPartial.add(item);
+                    temp.add(newPartial);
+                }
+            }
+            result = temp;
+        }
+        return result;
+    }
+}

+ 18 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductSkuServiceImpl.java

@@ -0,0 +1,18 @@
+package com.ylx.massage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.massage.domain.ProductSku;
+import com.ylx.massage.mapper.ProductSkuMapper;
+import com.ylx.massage.service.ProductSkuService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 商品SKU表(ProductSku)表服务实现类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Service("productSkuService")
+public class ProductSkuServiceImpl extends ServiceImpl<ProductSkuMapper, ProductSku> implements ProductSkuService {
+
+}

+ 18 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductSpecServiceImpl.java

@@ -0,0 +1,18 @@
+package com.ylx.massage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.massage.domain.ProductSpec;
+import com.ylx.massage.mapper.ProductSpecMapper;
+import com.ylx.massage.service.ProductSpecService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 商品规格表(ProductSpec)表服务实现类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Service("productSpecService")
+public class ProductSpecServiceImpl extends ServiceImpl<ProductSpecMapper, ProductSpec> implements ProductSpecService {
+
+}

+ 18 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/ProductSpecValueServiceImpl.java

@@ -0,0 +1,18 @@
+package com.ylx.massage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ylx.massage.domain.ProductSpecValue;
+import com.ylx.massage.mapper.ProductSpecValueMapper;
+import com.ylx.massage.service.ProductSpecValueService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 商品规格值表(ProductSpecValue)表服务实现类
+ *
+ * @author ylx
+ * @since 2026-03-26
+ */
+@Service("productSpecValueService")
+public class ProductSpecValueServiceImpl extends ServiceImpl<ProductSpecValueMapper, ProductSpecValue> implements ProductSpecValueService {
+
+}