共富商城

This commit is contained in:
zjf committed 2025-09-03 19:21:40 +08:00
1 parent 10e8120314
commit bd35814198
14 files changed
+1552 -8

No files matched your search

@@ -77,5 +77,6 @@
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,348 @@
# 商品新增接口使用说明
## 商品创建接口
### 接口地址
```
POST /mallProduct
```
### 请求参数
支持两种请求体类型:
#### 1. 包含SKU的创建(使用MallProductCreateDTO)
##### 多SKU模式示例(priceType = 1)
```json
{
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"deptId": 1,
"skuList": [
{
"skuCode": "IPHONE15-128-BLACK",
"skuName": "iPhone 15 128GB 黑色",
"price": 5999.00,
"costPrice": 4500.00,
"stock": 100,
"barcode": "1234567890123",
"weightG": 171,
"volumeCm3": 200,
"status": 1,
"deptId": 1
},
{
"skuCode": "IPHONE15-256-BLACK",
"skuName": "iPhone 15 256GB 黑色",
"price": 6999.00,
"costPrice": 5200.00,
"stock": 50,
"barcode": "1234567890124",
"weightG": 171,
"volumeCm3": 200,
"status": 1,
"deptId": 1
}
]
}
```
##### 单价SPU模式示例(priceType = 2)
```json
{
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 2,
"spuPrice": 5999.00,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"deptId": 1
}
```
#### 2. 普通SPU创建(使用MallProductEntity)
```json
{
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 2,
"spuPrice": 5999.00,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机"
}
```
### 响应示例
#### 成功响应
```json
{
"code": 0,
"msg": "商品创建成功",
"data": null
}
```
#### 失败响应
```json
{
"code": 1,
"msg": "商品创建失败:SKU编码不能为空",
"data": null
}
```
### 字段说明
#### SPU字段
- `merchantId`: 商户ID(必填)
- `categoryId`: 类目ID(必填)
- `title`: 商品标题(必填)
- `subTitle`: 副标题
- `brand`: 品牌
- `coverImage`: 主图
- `carouselImages`: SPU轮播图URL(逗号分隔)
- `priceType`: 定价方式(1=多SKU,2=单价SPU)(必填)
- `spuPrice`: SPU售价(当priceType=2时有效)
- `originalPrice`: 划线价
- `unit`: 计量单位
- `status`: 状态(1=上架,0=下架)
- `saleStartTime`: 开售时间
- `saleEndTime`: 停售时间
- `description`: 详情描述
- `deptId`: 部门ID
#### SKU字段(当priceType=1时必填)
- `skuCode`: SKU编码(必填)
- `skuName`: SKU名称(必填)
- `price`: 售价(必填)
- `costPrice`: 成本价
- `stock`: 库存(必填)
- `barcode`: 条码
- `weightG`: 重量(克)
- `volumeCm3`: 体积(立方厘米)
- `status`: 状态(1=启用,0=停用)
- `deptId`: 部门ID
### 业务逻辑说明
1. **多SKU模式(priceType = 1)**:
- 必须提供skuList
- 系统会自动计算总库存(所有SKU库存之和)
- 每个SKU都会保存到数据库
2. **单价SPU模式(priceType = 2)**:
- 不需要提供skuList
- 使用spuPrice作为商品价格
- 总库存设置为0(可根据业务需求调整)
3. **事务处理**:
- 整个创建过程在一个事务中执行
- 如果任何步骤失败,所有操作都会回滚
4. **数据验证**:
- 使用JSR-303验证注解进行参数校验
- 必填字段验证
- 业务逻辑验证
### 权限要求
需要 `supermarket_mallProduct_add` 权限
## 商品编辑接口
### 接口地址
```
PUT /mallProduct
```
### 请求参数
支持两种请求体类型:
#### 1. 包含SKU的编辑(使用MallProductCreateDTO)
```json
{
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机(已更新)",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"skuList": [
{
"skuCode": "IPHONE15-128-BLACK-UPDATED",
"skuName": "iPhone 15 128GB 黑色(已更新)",
"price": 5999.00,
"costPrice": 4500.00,
"stock": 100,
"barcode": "1234567890123",
"weightG": 171,
"volumeCm3": 200,
"status": 1
}
]
}
```
#### 2. 普通SPU编辑(使用MallProductEntity)
```json
{
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机(已更新)",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 2,
"spuPrice": 5999.00,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机"
}
```
### 响应示例
```json
{
"code": 0,
"msg": "商品更新成功",
"data": null
}
```
### 权限要求
需要 `supermarket_mallProduct_edit` 权限
## 商品详情查询接口
### 接口地址
```
GET /mallProduct/{id}
```
### 请求参数
- `id`: 商品ID(路径参数)
- `withSkus`: 是否包含SKU信息(查询参数,可选,默认false)
### 使用示例
#### 1. 查询基本信息(不包含SKU)
```
GET /mallProduct/1
```
#### 2. 查询完整信息(包含SKU)
```
GET /mallProduct/1?withSkus=true
```
### 响应示例
#### 基本信息响应
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机"
}
}
```
#### 完整信息响应(withSkus=true)
```json
{
"code": 0,
"msg": "操作成功",
"data": {
"id": 1,
"merchantId": 1,
"categoryId": 100,
"title": "苹果手机",
"subTitle": "最新款智能手机",
"brand": "Apple",
"coverImage": "https://example.com/cover.jpg",
"carouselImages": "https://example.com/img1.jpg,https://example.com/img2.jpg",
"priceType": 1,
"originalPrice": 9999.00,
"unit": "台",
"status": 1,
"saleStartTime": "2025-01-27T00:00:00",
"saleEndTime": "2025-12-31T23:59:59",
"description": "这是一款高性能的智能手机",
"skuList": [
{
"skuCode": "IPHONE15-128-BLACK",
"skuName": "iPhone 15 128GB 黑色",
"price": 5999.00,
"costPrice": 4500.00,
"stock": 100,
"barcode": "1234567890123",
"weightG": 171,
"volumeCm3": 200,
"status": 1
}
]
}
}
```
### 权限要求
需要 `supermarket_mallProduct_view` 权限
@@ -0,0 +1,308 @@
# 下拉选择接口使用说明
## 概述
为了方便后台管理系统使用下拉选择组件,我们提供了一系列统一的下拉选择接口。这些接口返回标准化的选项数据格式,便于前端组件使用。
## 统一返回格式
所有下拉选择接口都返回以下格式的数据:
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "选项名称",
"disabled": false,
"extra": "扩展信息"
}
]
}
```
### 字段说明
- `value`: 选项值,可以是数字、字符串等类型
- `label`: 选项显示名称
- `disabled`: 是否禁用,默认为false
- `extra`: 扩展信息,可以包含额外的数据
## 接口列表
### 1. 商户下拉选择
#### 接口地址
```
GET /selectOption/merchant?keyWord=关键字
```
#### 请求参数
- `keyWord` (可选): 关键字,支持商户名称和手机号模糊查询
#### 权限要求
需要 `supermarket_mallMerchant_view` 权限
#### 使用示例
```
# 获取所有商户
GET /selectOption/merchant
# 按商户名称搜索
GET /selectOption/merchant?keyWord=农场
# 按手机号搜索
GET /selectOption/merchant?keyWord=138
# 按关键字搜索(商户名称或手机号)
GET /selectOption/merchant?keyWord=张三
```
#### 响应示例
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "张三农场",
"disabled": false,
"extra": "13800138000"
},
{
"value": 2,
"label": "李四果园",
"disabled": false,
"extra": "13900139000"
}
]
}
```
### 2. 分类下拉选择
#### 接口地址
```
GET /selectOption/category
```
#### 权限要求
需要 `supermarket_mallCategory_view` 权限
#### 响应示例
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "水果",
"disabled": false,
"extra": 0
},
{
"value": 2,
"label": "蔬菜",
"disabled": false,
"extra": 0
},
{
"value": 3,
"label": "苹果",
"disabled": false,
"extra": 1
}
]
}
```
### 3. 分类下拉选择(树形结构)
#### 接口地址
```
GET /selectOption/category/tree
```
#### 权限要求
需要 `supermarket_mallCategory_view` 权限
#### 响应示例
```json
{
"code": 0,
"msg": "操作成功",
"data": [
{
"value": 1,
"label": "水果",
"disabled": false,
"extra": 0
},
{
"value": 3,
"label": " └─ 苹果",
"disabled": false,
"extra": 1
},
{
"value": 2,
"label": "蔬菜",
"disabled": false,
"extra": 0
}
]
}
```
## 传统接口
除了统一的下拉选择接口,原有的控制器也提供了下拉选择接口:
### 商户下拉选择(传统接口)
```
GET /mallMerchant/selectList
```
### 分类下拉选择(传统接口)
```
GET /mallCategory/selectList
```
## 前端使用示例
### Vue.js + Element Plus
```javascript
// 获取商户下拉选择
async getMerchantOptions(keyword = '') {
try {
const params = keyword ? { keyWord: keyword } : {};
const response = await this.$http.get('/selectOption/merchant', { params });
this.merchantOptions = response.data.data.map(item => ({
value: item.value,
label: item.label,
disabled: item.disabled
}));
} catch (error) {
console.error('获取商户选项失败:', error);
}
}
// 搜索商户
async searchMerchants(keyword) {
await this.getMerchantOptions(keyword);
}
// 获取分类下拉选择
async getCategoryOptions() {
try {
const response = await this.$http.get('/selectOption/category');
this.categoryOptions = response.data.data.map(item => ({
value: item.value,
label: item.label,
disabled: item.disabled
}));
} catch (error) {
console.error('获取分类选项失败:', error);
}
}
```
### HTML模板
```html
<template>
<div>
<!-- 商户选择(支持搜索) -->
<el-select
v-model="form.merchantId"
placeholder="请选择商户"
filterable
remote
:remote-method="searchMerchants"
:loading="merchantLoading">
<el-option
v-for="item in merchantOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
<span style="float: left">{{ item.label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.extra }}</span>
</el-option>
</el-select>
<!-- 分类选择 -->
<el-select v-model="form.categoryId" placeholder="请选择分类">
<el-option
v-for="item in categoryOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
</el-option>
</el-select>
</div>
</template>
<script>
export default {
data() {
return {
form: {
merchantId: null,
categoryId: null
},
merchantOptions: [],
categoryOptions: [],
merchantLoading: false
}
},
mounted() {
this.getMerchantOptions();
this.getCategoryOptions();
},
methods: {
// 搜索商户
async searchMerchants(keyword) {
this.merchantLoading = true;
try {
await this.getMerchantOptions(keyword);
} finally {
this.merchantLoading = false;
}
}
}
}
</script>
```
## 扩展说明
### 添加新的下拉选择类型
如果需要添加新的下拉选择类型,可以:
1. 在 `SelectOptionService` 中添加新的方法
2. 在 `SelectOptionController` 中添加对应的接口
3. 按照统一的格式返回数据
### 自定义扩展信息
`extra` 字段可以包含任何额外的信息,例如:
- 联系电话
- 父级ID
- 状态信息
- 其他业务相关数据
前端可以根据需要解析和使用这些扩展信息。
## 注意事项
1. 所有接口都需要相应的权限验证
2. 只返回启用状态的数据
3. 数据按照指定的排序规则返回
4. 建议前端对接口返回的数据进行缓存,避免频繁请求
@@ -155,4 +155,6 @@ public class MallCategoryController {
return R.ok(list);
}
}
@@ -120,4 +120,20 @@ public class MallMerchantController {
public List<MallMerchantEntity> export(MallMerchantEntity mallMerchant,Long[] ids) {
return mallMerchantService.list(Wrappers.lambdaQuery(mallMerchant).in(ArrayUtil.isNotEmpty(ids), MallMerchantEntity::getId, ids));
}
/**
* 获取商户下拉选择列表
* @return R
*/
@Operation(summary = "获取商户下拉选择列表" , description = "获取商户下拉选择列表" )
@GetMapping("/selectList")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')" )
public R getSelectList() {
LambdaQueryWrapper<MallMerchantEntity> wrapper = Wrappers.lambdaQuery();
wrapper.select(MallMerchantEntity::getId, MallMerchantEntity::getName, MallMerchantEntity::getContactName, MallMerchantEntity::getContactPhone);
wrapper.eq(MallMerchantEntity::getStatus, 1); // 只查询启用状态的商户
wrapper.orderByAsc(MallMerchantEntity::getName);
List<MallMerchantEntity> list = mallMerchantService.list(wrapper);
return R.ok(list);
}
}
@@ -10,6 +10,8 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.service.MallProductService;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -58,39 +60,85 @@ public class MallProductController {
/**
* 通过id查询共富商城商品信息
* @param id id
* @param withSkus 是否包含SKU信息
* @return R
*/
@Operation(summary = "通过id查询" , description = "通过id查询" )
@GetMapping("/{id}" )
@PreAuthorize("@pms.hasPermission('supermarket_mallProduct_view')" )
public R getById(@PathVariable("id" ) Long id) {
return R.ok(mallProductService.getById(id));
public R getById(@PathVariable("id" ) Long id, @RequestParam(value = "withSkus", defaultValue = "false") Boolean withSkus) {
try {
if (withSkus) {
MallProductCreateDTO productDTO = mallProductService.getProductWithSkus(id);
return R.ok(productDTO);
} else {
return R.ok(mallProductService.getById(id));
}
} catch (Exception e) {
return R.failed("获取商品详情失败:" + e.getMessage());
}
}
/**
* 新增共富商城商品信息
* @param mallProduct 共富商城商品信息
* @param requestBody 请求体(可以是MallProductEntity或MallProductCreateDTO)
* @return R
*/
@Operation(summary = "新增共富商城商品信息" , description = "新增共富商城商品信息" )
@SysLog("新增共富商城商品信息" )
@PostMapping
@PreAuthorize("@pms.hasPermission('supermarket_mallProduct_add')" )
public R save(@RequestBody MallProductEntity mallProduct) {
return R.ok(mallProductService.save(mallProduct));
public R save(@Valid @RequestBody Object requestBody) {
try {
// 判断请求体类型
if (requestBody instanceof MallProductCreateDTO) {
// 包含SKU的创建
MallProductCreateDTO productCreateDTO = (MallProductCreateDTO) requestBody;
boolean success = mallProductService.createProductWithSkus(productCreateDTO);
if (success) {
return R.ok("商品创建成功");
} else {
return R.failed("商品创建失败");
}
} else {
// 普通SPU创建
MallProductEntity mallProduct = (MallProductEntity) requestBody;
return R.ok(mallProductService.save(mallProduct));
}
} catch (Exception e) {
return R.failed("商品创建失败:" + e.getMessage());
}
}
/**
* 修改共富商城商品信息
* @param mallProduct 共富商城商品信息
* @param requestBody 请求体(可以是MallProductEntity或MallProductCreateDTO)
* @return R
*/
@Operation(summary = "修改共富商城商品信息" , description = "修改共富商城商品信息" )
@SysLog("修改共富商城商品信息" )
@PutMapping
@PreAuthorize("@pms.hasPermission('supermarket_mallProduct_edit')" )
public R updateById(@RequestBody MallProductEntity mallProduct) {
return R.ok(mallProductService.updateById(mallProduct));
public R updateById(@RequestBody Object requestBody) {
try {
// 判断请求体类型
if (requestBody instanceof MallProductCreateDTO) {
// 包含SKU的更新
MallProductCreateDTO productCreateDTO = (MallProductCreateDTO) requestBody;
boolean success = mallProductService.updateProductWithSkus(productCreateDTO);
if (success) {
return R.ok("商品更新成功");
} else {
return R.failed("商品更新失败");
}
} else {
// 普通SPU更新
MallProductEntity mallProduct = (MallProductEntity) requestBody;
return R.ok(mallProductService.updateById(mallProduct));
}
} catch (Exception e) {
return R.failed("商品更新失败:" + e.getMessage());
}
}
/**
@@ -0,0 +1,69 @@
package com.bsd.pjcloud.supermarket.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.dto.SelectOptionDTO;
import com.bsd.pjcloud.supermarket.entity.MallMerchantEntity;
import com.bsd.pjcloud.supermarket.service.SelectOptionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 下拉选择控制器
*
* @author zjf
* @date 2025-01-27
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/selectOption")
@Tag(description = "selectOption", name = "下拉选择管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SelectOptionController {
private final SelectOptionService selectOptionService;
/**
* 获取商户下拉选择列表
* @return R
*/
@Operation(summary = "获取商户下拉选择列表", description = "获取商户下拉选择列表")
@GetMapping("/merchant")
@PreAuthorize("@pms.hasPermission('supermarket_mallMerchant_view')")
public R<List<SelectOptionDTO>> getMerchantSelectList(@RequestParam(value = "keyWord",required = false)String keyWord) {
List<SelectOptionDTO> list = selectOptionService.getMerchantSelectList(keyWord);
return R.ok(list);
}
/**
* 获取分类下拉选择列表
* @return R
*/
@Operation(summary = "获取分类下拉选择列表", description = "获取分类下拉选择列表")
@GetMapping("/category")
@PreAuthorize("@pms.hasPermission('supermarket_mallCategory_view')")
public R<List<SelectOptionDTO>> getCategorySelectList() {
List<SelectOptionDTO> list = selectOptionService.getCategorySelectList();
return R.ok(list);
}
/**
* 获取分类下拉选择列表(树形结构)
* @return R
*/
@Operation(summary = "获取分类下拉选择列表(树形结构)", description = "获取分类下拉选择列表(树形结构)")
@GetMapping("/category/tree")
@PreAuthorize("@pms.hasPermission('supermarket_mallCategory_view')")
public R<List<SelectOptionDTO>> getCategoryTreeSelectList() {
List<SelectOptionDTO> list = selectOptionService.getCategoryTreeSelectList();
return R.ok(list);
}
}
@@ -0,0 +1,130 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 共富商城商品创建DTO(包含SPU和SKU信息)
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "共富商城商品创建DTO")
public class MallProductCreateDTO {
/**
* 主键(编辑时使用)
*/
@Schema(description = "主键")
private Long id;
/**
* 商户ID
*/
@NotNull(message = "商户ID不能为空")
@Schema(description = "商户ID")
private Long merchantId;
/**
* 类目ID
*/
@NotNull(message = "类目ID不能为空")
@Schema(description = "类目ID")
private Long categoryId;
/**
* 商品标题
*/
@NotBlank(message = "商品标题不能为空")
@Schema(description = "商品标题")
private String title;
/**
* 副标题
*/
@Schema(description = "副标题")
private String subTitle;
/**
* 品牌
*/
@Schema(description = "品牌")
private String brand;
/**
* 主图
*/
@Schema(description = "主图")
private String coverImage;
/**
* SPU轮播图URL(逗号分隔,按顺序)
*/
@Schema(description = "SPU轮播图URL(逗号分隔,按顺序)")
private String carouselImages;
/**
* 定价方式(1多SKU 2单价SPU)
*/
@NotNull(message = "定价方式不能为空")
@Schema(description = "定价方式(1多SKU 2单价SPU)")
private Integer priceType;
/**
* SPU售价(当price_type=2有效)
*/
@Schema(description = "SPU售价(当price_type=2有效)")
private BigDecimal spuPrice;
/**
* 划线价
*/
@Schema(description = "划线价")
private BigDecimal originalPrice;
/**
* 计量单位
*/
@Schema(description = "计量单位")
private String unit;
/**
* 状态(1上架 0下架)
*/
@Schema(description = "状态(1上架 0下架)")
private Integer status;
/**
* 开售时间
*/
@Schema(description = "开售时间")
private LocalDateTime saleStartTime;
/**
* 停售时间
*/
@Schema(description = "停售时间")
private LocalDateTime saleEndTime;
/**
* 详情描述(HTML/文本)
*/
@Schema(description = "详情描述(HTML/文本)")
private String description;
/**
* SKU列表(当priceType=1时必填)
*/
@Valid
@Schema(description = "SKU列表")
private List<MallProductSkuCreateDTO> skuList;
}
@@ -0,0 +1,78 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 共富商城商品SKU创建DTO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "共富商城商品SKU创建DTO")
public class MallProductSkuCreateDTO {
/**
* SKU编码
*/
@NotBlank(message = "SKU编码不能为空")
@Schema(description = "SKU编码")
private String skuCode;
/**
* SKU名称(规格组合)
*/
@NotBlank(message = "SKU名称不能为空")
@Schema(description = "SKU名称(规格组合)")
private String skuName;
/**
* 售价
*/
@NotNull(message = "售价不能为空")
@Schema(description = "售价")
private BigDecimal price;
/**
* 成本价
*/
@Schema(description = "成本价")
private BigDecimal costPrice;
/**
* 库存
*/
@NotNull(message = "库存不能为空")
@Schema(description = "库存")
private Integer stock;
/**
* 条码
*/
@Schema(description = "条码")
private String barcode;
/**
* 重量(克)
*/
@Schema(description = "重量(克)")
private Integer weightG;
/**
* 体积(立方厘米)
*/
@Schema(description = "体积(立方厘米)")
private Integer volumeCm3;
/**
* 状态(1启用 0停用)
*/
@Schema(description = "状态(1启用 0停用)")
private Integer status;
}
@@ -0,0 +1,66 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 下拉选择选项DTO
*
* @author zjf
* @date 2025-01-27
*/
@Data
@Schema(description = "下拉选择选项DTO")
public class SelectOptionDTO {
/**
* 选项值
*/
@Schema(description = "选项值")
private Object value;
/**
* 选项标签
*/
@Schema(description = "选项标签")
private String label;
/**
* 是否禁用
*/
@Schema(description = "是否禁用")
private Boolean disabled = false;
/**
* 扩展信息
*/
@Schema(description = "扩展信息")
private Object extra;
public SelectOptionDTO() {
}
public SelectOptionDTO(Object value, String label) {
this.value = value;
this.label = label;
}
public SelectOptionDTO(Object value, String label, Boolean disabled) {
this.value = value;
this.label = label;
this.disabled = disabled;
}
public SelectOptionDTO(Object value, String label, Object extra) {
this.value = value;
this.label = label;
this.extra = extra;
}
public SelectOptionDTO(Object value, String label, Boolean disabled, Object extra) {
this.value = value;
this.label = label;
this.disabled = disabled;
this.extra = extra;
}
}
@@ -2,7 +2,28 @@ package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
public interface MallProductService extends IService<MallProductEntity> {
/**
* 创建商品(包含SPU和SKU)
* @param productCreateDTO 商品创建DTO
* @return 是否成功
*/
boolean createProductWithSkus(MallProductCreateDTO productCreateDTO);
/**
* 更新商品(包含SPU和SKU)
* @param productCreateDTO 商品更新DTO
* @return 是否成功
*/
boolean updateProductWithSkus(MallProductCreateDTO productCreateDTO);
/**
* 获取商品详情(包含SPU和SKU)
* @param id 商品ID
* @return 商品详情DTO
*/
MallProductCreateDTO getProductWithSkus(Long id);
}
@@ -0,0 +1,102 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.supermarket.dto.SelectOptionDTO;
import com.bsd.pjcloud.supermarket.entity.MallCategoryEntity;
import com.bsd.pjcloud.supermarket.entity.MallMerchantEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 下拉选择服务
*
* @author zjf
* @date 2025-01-27
*/
@Service
@RequiredArgsConstructor
public class SelectOptionService {
private final MallMerchantService mallMerchantService;
private final MallCategoryService mallCategoryService;
/**
* 获取商户下拉选择列表
* @param keyword 关键字,支持商户名称和手机号模糊查询
* @return 商户下拉选择列表
*/
public List<SelectOptionDTO> getMerchantSelectList(String keyword) {
LambdaQueryWrapper<MallMerchantEntity> wrapper = Wrappers.lambdaQuery();
wrapper.select(MallMerchantEntity::getId, MallMerchantEntity::getName, MallMerchantEntity::getContactName, MallMerchantEntity::getContactPhone);
wrapper.eq(MallMerchantEntity::getStatus, 0); // 只查询启用状态的商户
// 如果关键字不为空,添加模糊查询条件
if (keyword != null && !keyword.trim().isEmpty()) {
wrapper.and(w -> w.like(MallMerchantEntity::getName, keyword.trim())
.or()
.like(MallMerchantEntity::getContactPhone, keyword.trim()));
}
wrapper.orderByAsc(MallMerchantEntity::getName);
return mallMerchantService.list(wrapper)
.stream()
.map(merchant -> new SelectOptionDTO(
merchant.getId(),
merchant.getName(),
false,
merchant.getContactPhone() // 扩展信息:联系电话
))
.collect(Collectors.toList());
}
/**
* 获取分类下拉选择列表
* @return 分类下拉选择列表
*/
public List<SelectOptionDTO> getCategorySelectList() {
return mallCategoryService.listScope()
.stream()
.filter(category -> category.getStatus() == 1) // 只返回启用状态的分类
.map(category -> new SelectOptionDTO(
category.getId(),
category.getName(),
false,
category.getParentId() // 扩展信息:父级ID
))
.collect(Collectors.toList());
}
/**
* 获取分类下拉选择列表(树形结构)
* @return 分类下拉选择列表
*/
public List<SelectOptionDTO> getCategoryTreeSelectList() {
List<MallCategoryEntity> allCategories = mallCategoryService.listScope();
return buildCategorySelectTree(allCategories, 0L, "");
}
/**
* 构建分类选择树
* @param allCategories 所有分类
* @param parentId 父级ID
* @param prefix 前缀
* @return 分类选择列表
*/
private List<SelectOptionDTO> buildCategorySelectTree(List<MallCategoryEntity> allCategories, Long parentId, String prefix) {
return allCategories.stream()
.filter(category -> category.getStatus() == 1 &&
(parentId == null ? category.getParentId() == null : parentId.equals(category.getParentId())))
.map(category -> {
String label = prefix + category.getName();
SelectOptionDTO option = new SelectOptionDTO(category.getId(), label);
option.setExtra(category.getParentId());
return option;
})
.collect(Collectors.toList());
}
}
@@ -1,10 +1,23 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.supermarket.entity.MallProductEntity;
import com.bsd.pjcloud.supermarket.entity.MallProductSkuEntity;
import com.bsd.pjcloud.supermarket.mapper.MallProductMapper;
import com.bsd.pjcloud.supermarket.service.MallProductService;
import com.bsd.pjcloud.supermarket.service.MallProductSkuService;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import com.bsd.pjcloud.supermarket.dto.MallProductSkuCreateDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* 共富商城商品信息
*
@@ -12,5 +25,243 @@ import org.springframework.stereotype.Service;
* @date 2025-09-02 17:47:01
*/
@Service
@RequiredArgsConstructor
public class MallProductServiceImpl extends ServiceImpl<MallProductMapper, MallProductEntity> implements MallProductService {
private final MallProductSkuService mallProductSkuService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean createProductWithSkus(MallProductCreateDTO productCreateDTO) {
// 业务验证
validateProductData(productCreateDTO);
// 1. 创建SPU
MallProductEntity product = new MallProductEntity();
product.setMerchantId(productCreateDTO.getMerchantId());
product.setCategoryId(productCreateDTO.getCategoryId());
product.setTitle(productCreateDTO.getTitle());
product.setSubTitle(productCreateDTO.getSubTitle());
product.setBrand(productCreateDTO.getBrand());
product.setCoverImage(productCreateDTO.getCoverImage());
product.setCarouselImages(productCreateDTO.getCarouselImages());
product.setPriceType(productCreateDTO.getPriceType());
product.setSpuPrice(productCreateDTO.getSpuPrice());
product.setOriginalPrice(productCreateDTO.getOriginalPrice());
product.setUnit(productCreateDTO.getUnit());
product.setStatus(productCreateDTO.getStatus());
product.setSaleStartTime(productCreateDTO.getSaleStartTime());
product.setSaleEndTime(productCreateDTO.getSaleEndTime());
product.setDescription(productCreateDTO.getDescription());
// 2. 保存SPU
boolean spuSaved = this.save(product);
if (!spuSaved) {
throw new RuntimeException("保存商品SPU失败");
}
// 3. 处理SKU
if (productCreateDTO.getPriceType() == 1 && CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
// 多SKU模式,保存SKU列表
List<MallProductSkuEntity> skuList = productCreateDTO.getSkuList().stream()
.map(skuDTO -> {
MallProductSkuEntity sku = new MallProductSkuEntity();
sku.setProductId(product.getId());
sku.setSkuCode(skuDTO.getSkuCode());
sku.setSkuName(skuDTO.getSkuName());
sku.setPrice(skuDTO.getPrice());
sku.setCostPrice(skuDTO.getCostPrice());
sku.setStock(skuDTO.getStock());
sku.setBarcode(skuDTO.getBarcode());
sku.setWeightG(skuDTO.getWeightG());
sku.setVolumeCm3(skuDTO.getVolumeCm3());
sku.setStatus(skuDTO.getStatus());
return sku;
})
.collect(Collectors.toList());
boolean skuSaved = mallProductSkuService.saveBatch(skuList);
if (!skuSaved) {
throw new RuntimeException("保存商品SKU失败");
}
// 计算总库存
int totalStock = skuList.stream()
.mapToInt(sku -> sku.getStock() != null ? sku.getStock() : 0)
.sum();
product.setTotalStock(totalStock);
this.updateById(product);
} else if (productCreateDTO.getPriceType() == 2) {
// 单价SPU模式,设置总库存为0(或根据业务需求设置)
product.setTotalStock(0);
this.updateById(product);
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateProductWithSkus(MallProductCreateDTO productCreateDTO) {
// 业务验证
validateProductData(productCreateDTO);
// 1. 更新SPU
MallProductEntity product = this.getById(productCreateDTO.getId());
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 更新SPU字段
product.setMerchantId(productCreateDTO.getMerchantId());
product.setCategoryId(productCreateDTO.getCategoryId());
product.setTitle(productCreateDTO.getTitle());
product.setSubTitle(productCreateDTO.getSubTitle());
product.setBrand(productCreateDTO.getBrand());
product.setCoverImage(productCreateDTO.getCoverImage());
product.setCarouselImages(productCreateDTO.getCarouselImages());
product.setPriceType(productCreateDTO.getPriceType());
product.setSpuPrice(productCreateDTO.getSpuPrice());
product.setOriginalPrice(productCreateDTO.getOriginalPrice());
product.setUnit(productCreateDTO.getUnit());
product.setStatus(productCreateDTO.getStatus());
product.setSaleStartTime(productCreateDTO.getSaleStartTime());
product.setSaleEndTime(productCreateDTO.getSaleEndTime());
product.setDescription(productCreateDTO.getDescription());
// 2. 保存SPU
boolean spuUpdated = this.updateById(product);
if (!spuUpdated) {
throw new RuntimeException("更新商品SPU失败");
}
// 3. 处理SKU
if (productCreateDTO.getPriceType() == 1 && CollUtil.isNotEmpty(productCreateDTO.getSkuList())) {
// 多SKU模式,先删除原有SKU,再保存新的SKU列表
LambdaQueryWrapper<MallProductSkuEntity> deleteWrapper = Wrappers.lambdaQuery();
deleteWrapper.eq(MallProductSkuEntity::getProductId, product.getId());
mallProductSkuService.remove(deleteWrapper);
List<MallProductSkuEntity> skuList = productCreateDTO.getSkuList().stream()
.map(skuDTO -> {
MallProductSkuEntity sku = new MallProductSkuEntity();
sku.setProductId(product.getId());
sku.setSkuCode(skuDTO.getSkuCode());
sku.setSkuName(skuDTO.getSkuName());
sku.setPrice(skuDTO.getPrice());
sku.setCostPrice(skuDTO.getCostPrice());
sku.setStock(skuDTO.getStock());
sku.setBarcode(skuDTO.getBarcode());
sku.setWeightG(skuDTO.getWeightG());
sku.setVolumeCm3(skuDTO.getVolumeCm3());
sku.setStatus(skuDTO.getStatus());
return sku;
})
.collect(Collectors.toList());
boolean skuSaved = mallProductSkuService.saveBatch(skuList);
if (!skuSaved) {
throw new RuntimeException("保存商品SKU失败");
}
// 计算总库存
int totalStock = skuList.stream()
.mapToInt(sku -> sku.getStock() != null ? sku.getStock() : 0)
.sum();
product.setTotalStock(totalStock);
this.updateById(product);
} else if (productCreateDTO.getPriceType() == 2) {
// 单价SPU模式,删除所有SKU
LambdaQueryWrapper<MallProductSkuEntity> deleteWrapper = Wrappers.lambdaQuery();
deleteWrapper.eq(MallProductSkuEntity::getProductId, product.getId());
mallProductSkuService.remove(deleteWrapper);
product.setTotalStock(0);
this.updateById(product);
}
return true;
}
@Override
public MallProductCreateDTO getProductWithSkus(Long id) {
// 1. 获取SPU信息
MallProductEntity product = this.getById(id);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 2. 转换为DTO
MallProductCreateDTO dto = new MallProductCreateDTO();
dto.setId(product.getId());
dto.setMerchantId(product.getMerchantId());
dto.setCategoryId(product.getCategoryId());
dto.setTitle(product.getTitle());
dto.setSubTitle(product.getSubTitle());
dto.setBrand(product.getBrand());
dto.setCoverImage(product.getCoverImage());
dto.setCarouselImages(product.getCarouselImages());
dto.setPriceType(product.getPriceType());
dto.setSpuPrice(product.getSpuPrice());
dto.setOriginalPrice(product.getOriginalPrice());
dto.setUnit(product.getUnit());
dto.setStatus(product.getStatus());
dto.setSaleStartTime(product.getSaleStartTime());
dto.setSaleEndTime(product.getSaleEndTime());
dto.setDescription(product.getDescription());
// 3. 获取SKU信息
if (product.getPriceType() == 1) {
LambdaQueryWrapper<MallProductSkuEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(MallProductSkuEntity::getProductId, id);
List<MallProductSkuEntity> skuList = mallProductSkuService.list(wrapper);
List<MallProductSkuCreateDTO> skuDTOList = skuList.stream()
.map(sku -> {
MallProductSkuCreateDTO skuDTO = new MallProductSkuCreateDTO();
skuDTO.setSkuCode(sku.getSkuCode());
skuDTO.setSkuName(sku.getSkuName());
skuDTO.setPrice(sku.getPrice());
skuDTO.setCostPrice(sku.getCostPrice());
skuDTO.setStock(sku.getStock());
skuDTO.setBarcode(sku.getBarcode());
skuDTO.setWeightG(sku.getWeightG());
skuDTO.setVolumeCm3(sku.getVolumeCm3());
skuDTO.setStatus(sku.getStatus());
return skuDTO;
})
.collect(Collectors.toList());
dto.setSkuList(skuDTOList);
}
return dto;
}
/**
* 商品业务数据验证
* @param productCreateDTO 商品创建DTO
*/
private void validateProductData(MallProductCreateDTO productCreateDTO) {
// 多SKU模式验证
if (productCreateDTO.getPriceType() == 1) {
if (CollUtil.isEmpty(productCreateDTO.getSkuList())) {
throw new RuntimeException("多SKU模式下必须提供SKU列表");
}
// 验证SKU编码唯一性
long distinctSkuCodes = productCreateDTO.getSkuList().stream()
.map(MallProductSkuCreateDTO::getSkuCode)
.distinct()
.count();
if (distinctSkuCodes != productCreateDTO.getSkuList().size()) {
throw new RuntimeException("SKU编码不能重复");
}
}
// 单价SPU模式验证
if (productCreateDTO.getPriceType() == 2) {
if (productCreateDTO.getSpuPrice() == null || productCreateDTO.getSpuPrice().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("单价SPU模式下必须设置有效的SPU价格");
}
}
}
}
@@ -0,0 +1,104 @@
package com.bsd.pjcloud.supermarket.service;
import com.bsd.pjcloud.supermarket.dto.MallProductCreateDTO;
import com.bsd.pjcloud.supermarket.dto.MallProductSkuCreateDTO;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* 商品服务测试类
*
* @author zjf
* @date 2025-01-27
*/
@SpringBootTest
@ActiveProfiles("test")
public class MallProductServiceTest {
@Resource
private MallProductService mallProductService;
@Test
public void testCreateProductWithSkus() {
// 创建测试数据
MallProductCreateDTO productCreateDTO = new MallProductCreateDTO();
productCreateDTO.setMerchantId(1L);
productCreateDTO.setCategoryId(100L);
productCreateDTO.setTitle("测试商品");
productCreateDTO.setSubTitle("测试副标题");
productCreateDTO.setBrand("测试品牌");
productCreateDTO.setCoverImage("https://example.com/cover.jpg");
productCreateDTO.setCarouselImages("https://example.com/img1.jpg,https://example.com/img2.jpg");
productCreateDTO.setPriceType(1); // 多SKU模式
productCreateDTO.setOriginalPrice(new BigDecimal("999.00"));
productCreateDTO.setUnit("台");
productCreateDTO.setStatus(1);
productCreateDTO.setSaleStartTime(LocalDateTime.now());
productCreateDTO.setSaleEndTime(LocalDateTime.now().plusYears(1));
productCreateDTO.setDescription("测试商品描述");
productCreateDTO.setDeptId(1L);
// 创建SKU列表
MallProductSkuCreateDTO sku1 = new MallProductSkuCreateDTO();
sku1.setSkuCode("TEST-SKU-001");
sku1.setSkuName("测试SKU 1");
sku1.setPrice(new BigDecimal("599.00"));
sku1.setCostPrice(new BigDecimal("400.00"));
sku1.setStock(100);
sku1.setBarcode("1234567890123");
sku1.setWeightG(200);
sku1.setVolumeCm3(300);
sku1.setStatus(1);
sku1.setDeptId(1L);
MallProductSkuCreateDTO sku2 = new MallProductSkuCreateDTO();
sku2.setSkuCode("TEST-SKU-002");
sku2.setSkuName("测试SKU 2");
sku2.setPrice(new BigDecimal("699.00"));
sku2.setCostPrice(new BigDecimal("500.00"));
sku2.setStock(50);
sku2.setBarcode("1234567890124");
sku2.setWeightG(250);
sku2.setVolumeCm3(350);
sku2.setStatus(1);
sku2.setDeptId(1L);
productCreateDTO.setSkuList(Arrays.asList(sku1, sku2));
// 执行测试
boolean result = mallProductService.createProductWithSkus(productCreateDTO);
assert result : "商品创建应该成功";
}
@Test
public void testCreateProductWithSinglePrice() {
// 创建测试数据 - 单价SPU模式
MallProductCreateDTO productCreateDTO = new MallProductCreateDTO();
productCreateDTO.setMerchantId(1L);
productCreateDTO.setCategoryId(100L);
productCreateDTO.setTitle("测试商品-单价模式");
productCreateDTO.setSubTitle("测试副标题");
productCreateDTO.setBrand("测试品牌");
productCreateDTO.setCoverImage("https://example.com/cover.jpg");
productCreateDTO.setCarouselImages("https://example.com/img1.jpg");
productCreateDTO.setPriceType(2); // 单价SPU模式
productCreateDTO.setSpuPrice(new BigDecimal("599.00"));
productCreateDTO.setOriginalPrice(new BigDecimal("999.00"));
productCreateDTO.setUnit("台");
productCreateDTO.setStatus(1);
productCreateDTO.setSaleStartTime(LocalDateTime.now());
productCreateDTO.setSaleEndTime(LocalDateTime.now().plusYears(1));
productCreateDTO.setDescription("测试商品描述");
productCreateDTO.setDeptId(1L);
// 执行测试
boolean result = mallProductService.createProductWithSkus(productCreateDTO);
assert result : "商品创建应该成功";
}
}