feat(product): 新增产品管理功能模块

- 新增产品管理控制器ProductController,提供创建、更新、删除、详情、分页查询等接口
- 新增产品服务接口ProductServiceI及实现类ProductServiceImpl,用于业务逻辑处理
- 新增产品相关命令和查询对象,如ProductCreateCmd、ProductUpdateCmd、ProductPageQry等
- 新增产品实体类Product及其数据传输对象ProductVO和ProductDO
- 新增产品网关接口ProductGateway及实现类ProductGatewayImpl,封装数据访问逻辑
- 新增产品转换器ProductConvert,支持DO与Entity之间的映射
- 新增产品Mapper接口ProductMapper,继承BaseMapper并扩展批量重置排序方法
- 完成产品模块从控制层到数据访问层的完整链路开发与集成
- 支持产品拖拽排序功能,包括前后位置计算与间隙不足时的重新排序逻辑
This commit is contained in:
shenyifei 2025-11-14 13:26:55 +08:00
parent ab0151e84d
commit 7e58fc8d62
27 changed files with 1080 additions and 0 deletions

View File

@ -0,0 +1,95 @@
package com.xunhong.erp.turbo.admin.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
import com.xunhong.erp.turbo.api.biz.api.ProductServiceI;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDragCmd;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductShowQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.base.dto.PageDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* @author shenyifei
*/
@Tag(name = "Product", description = "产品管理")
@RestController("operationProductController")
@RequestMapping(value = "/operation")
@RequiredArgsConstructor
public class ProductController {
@DubboReference(version = "1.0.0")
private final ProductServiceI productService;
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_VIEW})
@GetMapping("listProduct")
@Operation(summary = "产品列表", method = "GET")
public MultiResponse<ProductVO> listProduct(@ModelAttribute @Validated ProductListQry productListQry) {
return MultiResponse.of(productService.list(productListQry));
}
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_CREATE})
@PostMapping("createProduct")
@Operation(summary = "创建产品", method = "POST")
public SingleResponse<ProductVO> createProduct(@RequestBody @Validated ProductCreateCmd productCreateCmd) {
return SingleResponse.of(productService.create(productCreateCmd));
}
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_VIEW})
@GetMapping("showProduct")
@Operation(summary = "产品详情", method = "GET")
public SingleResponse<ProductVO> showProduct(@ModelAttribute @Validated ProductShowQry productShowQry) {
return SingleResponse.of(productService.show(productShowQry));
}
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_VIEW})
@GetMapping("pageProduct")
@Operation(summary = "产品列表", method = "GET")
public PageResponse<ProductVO> pageProduct(@ModelAttribute @Validated ProductPageQry productPageQry) {
PageDTO<ProductVO> page = productService.page(productPageQry);
return PageResponse.of(page.getRecords(), (int) page.getTotal(), (int) page.getSize(), (int) page.getCurrent());
}
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_UPDATE})
@RequestMapping(value = "updateProduct", method = {RequestMethod.PATCH, RequestMethod.PUT})
@Operation(summary = "产品更新", method = "PATCH")
public SingleResponse<ProductVO> updateProduct(@RequestBody @Validated ProductUpdateCmd productUpdateCmd) {
return SingleResponse.of(productService.update(productUpdateCmd));
}
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_DELETE})
@DeleteMapping("destroyProduct")
@Operation(summary = "产品删除", method = "DELETE")
public Response destroyProduct(@RequestBody @Validated ProductDestroyCmd productDestroyCmd) {
productService.destroy(productDestroyCmd);
return Response.buildSuccess();
}
@SaCheckLogin
// @SaCheckPermission(value = {PermissionConstant.MDB_BUSINESS_COST_ITEM_UPDATE})
@RequestMapping(value = "dragProduct", method = {RequestMethod.PATCH, RequestMethod.PUT})
@Operation(summary = "产品拖拽排序", method = "PATCH")
public Response dragProduct(@RequestBody @Validated ProductDragCmd productDragCmd) {
productService.drag(productDragCmd);
return Response.buildSuccess();
}
}

View File

@ -0,0 +1,17 @@
package com.xunhong.erp.turbo.biz.app.assembler;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.NullValueCheckStrategy;
/**
* @author shenyifei
*/
@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface ProductAssembler {
@Mapping(target = "costItemVOList", source = "costItemList")
ProductVO toProductVO(Product product);
}

View File

@ -0,0 +1,29 @@
package com.xunhong.erp.turbo.biz.app.executor.cmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.biz.app.assembler.ProductAssembler;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductCreateCmdExe {
private final ProductAssembler productAssembler;
private final ProductGateway productGateway;
public ProductVO execute(ProductCreateCmd productCreateCmd) {
Product product = productGateway.save(productCreateCmd);
return productAssembler.toProductVO(product);
}
}

View File

@ -0,0 +1,23 @@
package com.xunhong.erp.turbo.biz.app.executor.cmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDestroyCmd;
import com.xunhong.erp.turbo.biz.app.assembler.ProductAssembler;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductDestroyCmdExe {
private final ProductGateway productGateway;
public void execute(ProductDestroyCmd productDestroyCmd) {
productGateway.destroy(productDestroyCmd);
}
}

View File

@ -0,0 +1,27 @@
package com.xunhong.erp.turbo.biz.app.executor.cmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.biz.app.assembler.ProductAssembler;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductUpdateCmdExe {
private final ProductAssembler productAssembler;
private final ProductGateway productGateway;
public ProductVO execute(ProductUpdateCmd productUpdateCmd) {
Product product = productGateway.update(productUpdateCmd);
return productAssembler.toProductVO(product);
}
}

View File

@ -0,0 +1,21 @@
package com.xunhong.erp.turbo.biz.app.executor.query;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDragCmd;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductDragCmdExe {
private final ProductGateway productGateway;
public void execute(ProductDragCmd productDragCmd) {
productGateway.drag(productDragCmd);
}
}

View File

@ -0,0 +1,30 @@
package com.xunhong.erp.turbo.biz.app.executor.query;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductListQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.biz.app.assembler.ProductAssembler;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductListQryExe {
private final ProductGateway productGateway;
private final ProductAssembler productAssembler;
public List<ProductVO> execute(ProductListQry productListQry) {
List<Product> productList = productGateway.list(productListQry);
return productList.stream().map(productAssembler::toProductVO).toList();
}
}

View File

@ -0,0 +1,29 @@
package com.xunhong.erp.turbo.biz.app.executor.query;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductPageQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.biz.app.assembler.ProductAssembler;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductPageQryExe {
private final ProductGateway productGateway;
private final ProductAssembler productAssembler;
public IPage<ProductVO> execute(ProductPageQry productPageQry) {
IPage<Product> page = productGateway.page(productPageQry);
return page.convert(productAssembler::toProductVO);
}
}

View File

@ -0,0 +1,29 @@
package com.xunhong.erp.turbo.biz.app.executor.query;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductShowQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.biz.app.assembler.ProductAssembler;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author shenyifei
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductShowQryExe {
private final ProductAssembler productAssembler;
private final ProductGateway productGateway;
public ProductVO execute(ProductShowQry productShowQry) {
Product product = productGateway.show(productShowQry);
return productAssembler.toProductVO(product);
}
}

View File

@ -0,0 +1,78 @@
package com.xunhong.erp.turbo.biz.app.service;
import com.xunhong.erp.turbo.api.biz.api.ProductServiceI;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDragCmd;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductShowQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.base.dto.PageDTO;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ProductCreateCmdExe;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ProductDestroyCmdExe;
import com.xunhong.erp.turbo.biz.app.executor.cmd.ProductUpdateCmdExe;
import com.xunhong.erp.turbo.biz.app.executor.query.ProductListQryExe;
import com.xunhong.erp.turbo.biz.app.executor.query.ProductPageQryExe;
import com.xunhong.erp.turbo.biz.app.executor.query.ProductShowQryExe;
import com.xunhong.erp.turbo.biz.app.executor.query.ProductDragCmdExe;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author shenyifei
*/
@Slf4j
@Service
@DubboService(interfaceClass = ProductServiceI.class, version = "1.0.0")
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductServiceI {
private final ProductCreateCmdExe productCreateCmdExe;
private final ProductUpdateCmdExe productUpdateCmdExe;
private final ProductPageQryExe productPageQryExe;
private final ProductListQryExe productListQryExe;
private final ProductShowQryExe productShowQryExe;
private final ProductDestroyCmdExe productDestroyCmdExe;
private final ProductDragCmdExe productDragCmdExe;
@Override
public ProductVO create(ProductCreateCmd productCreateCmd) {
return productCreateCmdExe.execute(productCreateCmd);
}
@Override
public PageDTO<ProductVO> page(ProductPageQry productPageQry) {
return PageDTO.of(productPageQryExe.execute(productPageQry));
}
@Override
public List<ProductVO> list(ProductListQry productListQry) {
return productListQryExe.execute(productListQry);
}
@Override
public ProductVO update(ProductUpdateCmd productUpdateCmd) {
return productUpdateCmdExe.execute(productUpdateCmd);
}
@Override
public ProductVO show(ProductShowQry productShowQry) {
return productShowQryExe.execute(productShowQry);
}
@Override
public void destroy(ProductDestroyCmd productDestroyCmd) {
productDestroyCmdExe.execute(productDestroyCmd);
}
@Override
public void drag(ProductDragCmd productDragCmd) {
productDragCmdExe.execute(productDragCmd);
}
}

View File

@ -0,0 +1,61 @@
package com.xunhong.erp.turbo.biz.domain.entity;
import com.alibaba.cola.domain.Entity;
import com.alibaba.cola.dto.DTO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.*;
import java.time.*;
import java.util.List;
/**
* @author shenyifei
*/
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
public class Product extends DTO {
/**
* 产品ID
*/
private Long productId;
/**
* 产品名称
*/
private String name;
/**
* 关联成本费用id
*/
private List<Long> costItemIds;
/**
* 备注
*/
private String remark;
/**
* 排序号
*/
private BigDecimal sort;
/**
* 状态1_启用0_禁用
*/
private Boolean status;
/**
* 成本费用列表
*/
private List<CostItem> costItemList;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,32 @@
package com.xunhong.erp.turbo.biz.domain.gateway;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDragCmd;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductShowQry;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import java.util.List;
/**
* @author shenyifei
*/
public interface ProductGateway {
Product save(ProductCreateCmd productCreateCmd);
IPage<Product> page(ProductPageQry productPageQry);
List<Product> list(ProductListQry productListQry);
Product update(ProductUpdateCmd productUpdateCmd);
Product show(ProductShowQry productShowQry);
void destroy(ProductDestroyCmd productDestroyCmd);
void drag(ProductDragCmd productDragCmd);
}

View File

@ -0,0 +1,35 @@
package com.xunhong.erp.turbo.biz.infrastructure.convert;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.infrastructure.entity.ProductDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValueCheckStrategy;
/**
* @author shenyifei
*/
@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface ProductConvert {
@Mapping(target = "costItemList", source = "costItemDOList")
Product toProduct(ProductDO productDO);
@Mapping(target = "costItemDOList", ignore = true)
@Mapping(target = "version", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "isDelete", ignore = true)
@Mapping(target = "createdAt", ignore = true)
ProductDO toProductDO(ProductCreateCmd productCreateCmd);
@Mapping(target = "costItemDOList", ignore = true)
@Mapping(target = "version", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "isDelete", ignore = true)
@Mapping(target = "createdAt", ignore = true)
void toProductDO(@MappingTarget ProductDO productDO, ProductUpdateCmd productUpdateCmd);
}

View File

@ -0,0 +1,70 @@
package com.xunhong.erp.turbo.biz.infrastructure.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.xunhong.erp.turbo.datasource.domain.entity.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author shenyifei
*/
@Data
@TableName(value = "product", autoResultMap = true)
@EqualsAndHashCode(callSuper = true)
public class ProductDO extends BaseDO<ProductDO> {
/**
* 产品ID
*/
@TableId(value = "product_id", type = IdType.ASSIGN_ID)
private Long productId;
/**
* 产品名称
*/
@TableField(value = "name")
private String name;
/**
* 关联成本费用id
*/
@TableField(value = "cost_item_ids", typeHandler = JacksonTypeHandler.class)
private List<Long> costItemIds;
/**
* 备注
*/
@TableField(value = "remark")
private String remark;
/**
* 排序号
*/
@TableField(value = "sort")
private BigDecimal sort;
/**
* 状态1_启用0_禁用
*/
@TableField(value = "status")
private Boolean status;
@TableField(exist = false)
private List<CostItemDO> costItemDOList;
/**
* 创建时间
*/
@TableField(value = "created_at")
private LocalDateTime createdAt;
}

View File

@ -14,6 +14,7 @@ import com.xunhong.erp.turbo.api.biz.dto.qry.CompanyShowQry;
import com.xunhong.erp.turbo.biz.domain.entity.Company;
import com.xunhong.erp.turbo.biz.domain.gateway.CompanyGateway;
import com.xunhong.erp.turbo.biz.infrastructure.convert.CompanyConvert;
import com.xunhong.erp.turbo.biz.infrastructure.entity.BoxProductDO;
import com.xunhong.erp.turbo.biz.infrastructure.entity.CompanyDO;
import com.xunhong.erp.turbo.biz.infrastructure.mapper.CompanyMapper;
import lombok.RequiredArgsConstructor;

View File

@ -0,0 +1,167 @@
package com.xunhong.erp.turbo.biz.infrastructure.gateway;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDragCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductShowQry;
import com.xunhong.erp.turbo.biz.domain.entity.Product;
import com.xunhong.erp.turbo.biz.domain.gateway.ProductGateway;
import com.xunhong.erp.turbo.biz.infrastructure.convert.ProductConvert;
import com.xunhong.erp.turbo.biz.infrastructure.entity.CostItemDO;
import com.xunhong.erp.turbo.biz.infrastructure.entity.ProductDO;
import com.xunhong.erp.turbo.biz.infrastructure.mapper.CostItemMapper;
import com.xunhong.erp.turbo.biz.infrastructure.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author shenyifei
*/
@Repository
@RequiredArgsConstructor
public class ProductGatewayImpl implements ProductGateway {
private final ProductMapper productMapper;
private final ProductConvert productConvert;
private final CostItemMapper costItemMapper;
@Override
public Product save(ProductCreateCmd productCreateCmd) {
ProductDO productDO = productConvert.toProductDO(productCreateCmd);
productMapper.insert(productDO);
return productConvert.toProduct(productDO);
}
@Override
public IPage<Product> page(ProductPageQry productPageQry) {
LambdaQueryWrapper<ProductDO> queryWrapper = Wrappers.lambdaQuery(ProductDO.class);
queryWrapper.eq(Objects.nonNull(productPageQry.getStatus()), ProductDO::getStatus, productPageQry.getStatus());
queryWrapper.orderByAsc(ProductDO::getSort);
queryWrapper.orderByDesc(ProductDO::getCreatedAt);
IPage<ProductDO> page = new Page<>(productPageQry.getPageIndex(), productPageQry.getPageSize());
page = productMapper.selectPage(page, queryWrapper);
return page.convert(productConvert::toProduct);
}
@Override
public List<Product> list(ProductListQry productListQry) {
LambdaQueryWrapper<ProductDO> queryWrapper = Wrappers.lambdaQuery(ProductDO.class);
queryWrapper.eq(Objects.nonNull(productListQry.getStatus()), ProductDO::getStatus, productListQry.getStatus());
queryWrapper.orderByAsc(ProductDO::getSort);
queryWrapper.orderByDesc(ProductDO::getCreatedAt);
List<ProductDO> productDOList = productMapper.selectList(queryWrapper);
Set<Long> costItemIdList = productDOList.stream().map(ProductDO::getCostItemIds).flatMap(List::stream).collect(Collectors.toSet());
if (CollUtil.isNotEmpty(costItemIdList)) {
LambdaQueryWrapper<CostItemDO> queryWrapper1 = Wrappers.lambdaQuery(CostItemDO.class);
queryWrapper1.in(CostItemDO::getItemId, costItemIdList);
List<CostItemDO> costItemDOList = costItemMapper.selectList(queryWrapper1);
productDOList.forEach(productDO -> {
productDO.setCostItemDOList(costItemDOList.stream().filter(costItemDO -> productDO.getCostItemIds().contains(costItemDO.getItemId())).toList());
});
}
return productDOList.stream().map(productConvert::toProduct).toList();
}
@Override
public Product update(ProductUpdateCmd productUpdateCmd) {
LambdaQueryWrapper<ProductDO> queryWrapper = Wrappers.lambdaQuery(ProductDO.class);
queryWrapper.eq(ProductDO::getProductId, productUpdateCmd.getProductId());
queryWrapper.last("limit 1");
ProductDO productDO = productMapper.selectOne(queryWrapper);
productConvert.toProductDO(productDO, productUpdateCmd);
productMapper.updateById(productDO);
return productConvert.toProduct(productDO);
}
@Override
public Product show(ProductShowQry productShowQry) {
LambdaQueryWrapper<ProductDO> queryWrapper = Wrappers.lambdaQuery(ProductDO.class);
queryWrapper.eq(ProductDO::getProductId, productShowQry.getProductId());
queryWrapper.last("limit 1");
ProductDO productDO = productMapper.selectOne(queryWrapper);
return productConvert.toProduct(productDO);
}
@Override
public void destroy(ProductDestroyCmd productDestroyCmd) {
LambdaQueryWrapper<ProductDO> queryWrapper = Wrappers.lambdaQuery(ProductDO.class);
queryWrapper.eq(ProductDO::getProductId, productDestroyCmd.getProductId());
queryWrapper.last("limit 1");
ProductDO productDO = productMapper.selectOne(queryWrapper);
productDO.deleteById();
}
@Override
@Transactional
public void drag(ProductDragCmd productDragCmd) {
reorderTask(productDragCmd.getCurrentId(), productDragCmd.getPrevId(), productDragCmd.getNextId());
}
// 拖拽排序核心方法
public void reorderTask(Long currentId, Long prevId, Long nextId) {
// 获取相邻元素的sort值
Double prevSort = Optional.ofNullable(prevId)
.map(id -> productMapper.selectById(id).getSort().doubleValue())
.orElse(null);
Double nextSort = Optional.ofNullable(nextId)
.map(id -> productMapper.selectById(id).getSort().doubleValue())
.orElse(null);
// 计算新sort值
Double newSort = calculateNewSort(prevSort, nextSort);
// 检查是否需要重整排序
if (needResetSort(prevSort, nextSort)) {
productMapper.batchResetSort(productMapper.selectById(currentId).getProductId());
} else {
// 更新当前任务sort值
productMapper.update(null, new LambdaUpdateWrapper<ProductDO>()
.eq(ProductDO::getProductId, currentId)
.set(ProductDO::getSort, newSort)
);
}
}
private Double calculateNewSort(Double prevSort, Double nextSort) {
if (prevSort == null && nextSort != null) {
return nextSort - 1000; // 插入到开头
} else if (nextSort == null && prevSort != null) {
return prevSort + 1000; // 插入到末尾
} else if (prevSort == null && nextSort == null) {
return 0.0;
} else {
return (prevSort + nextSort) / 2; // 插入中间
}
}
private boolean needResetSort(Double prevSort, Double nextSort) {
return prevSort != null && nextSort != null
&& (nextSort - prevSort) < 1.0; // 判断间隙是否耗尽
}
}

View File

@ -0,0 +1,23 @@
package com.xunhong.erp.turbo.biz.infrastructure.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xunhong.erp.turbo.biz.infrastructure.entity.ProductDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* @author shenyifei
*/
@Mapper
public interface ProductMapper extends BaseMapper<ProductDO> {
/**
* 批量重置排序
*
* @param productId 产品ID
*/
@Update("UPDATE product SET sort = (product_id * 1000) WHERE product_id = #{productId}")
void batchResetSort(@Param("productId") Long productId);
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace="com.xunhong.erp.turbo.biz.infrastructure.mapper.ProductMapper">
<resultMap id="BaseResultMap"
type="com.xunhong.erp.turbo.biz.infrastructure.entity.ProductDO">
<result property="productId" column="product_id"/>
<result property="name" column="name"/>
<result property="costItemIds" column="cost_item_ids"
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<result property="remark" column="remark"/>
<result property="sort" column="sort"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="isDelete" column="is_delete"/>
<result property="version" column="version"/>
</resultMap>
</mapper>

View File

@ -0,0 +1,32 @@
package com.xunhong.erp.turbo.api.biz.api;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductCreateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDestroyCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductUpdateCmd;
import com.xunhong.erp.turbo.api.biz.dto.cmd.ProductDragCmd;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductListQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductPageQry;
import com.xunhong.erp.turbo.api.biz.dto.qry.ProductShowQry;
import com.xunhong.erp.turbo.api.biz.dto.vo.ProductVO;
import com.xunhong.erp.turbo.base.dto.PageDTO;
import java.util.List;
/**
* @author shenyifei
*/
public interface ProductServiceI {
ProductVO create(ProductCreateCmd productCreateCmd);
PageDTO<ProductVO> page(ProductPageQry productPageQry);
List<ProductVO> list(ProductListQry productListQry);
ProductVO update(ProductUpdateCmd productUpdateCmd);
ProductVO show(ProductShowQry productShowQry);
void destroy(ProductDestroyCmd productDestroyCmd);
void drag(ProductDragCmd productDragCmd);
}

View File

@ -0,0 +1,63 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.*;
import java.time.*;
import java.util.List;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品表创建")
@EqualsAndHashCode(callSuper = true)
public class ProductCreateCmd extends Command {
/**
* 产品ID
*/
@Schema(title = "产品ID", type = "string", requiredMode = Schema.RequiredMode.REQUIRED)
private Long productId;
/**
* 产品名称
*/
@Schema(title = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
/**
* 关联成本费用id
*/
@Schema(title = "关联成本费用id")
private List<Long> costItemIds;
/**
* 备注
*/
@Schema(title = "备注")
private String remark;
/**
* 排序号
*/
@Schema(title = "排序号", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal sort;
/**
* 状态1_启用0_禁用
*/
@Schema(title = "状态1_启用0_禁用", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean status;
/**
* 创建时间
*/
@Schema(title = "创建时间")
private LocalDateTime createdAt;
}

View File

@ -0,0 +1,19 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "删除产品表")
@EqualsAndHashCode(callSuper = true)
public class ProductDestroyCmd extends Command {
@Schema(title = "产品表ID", requiredMode = Schema.RequiredMode.REQUIRED, type = "string")
private Long productId;
}

View File

@ -0,0 +1,33 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import com.xunhong.erp.turbo.base.dto.Command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品拖拽")
@EqualsAndHashCode(callSuper = true)
public class ProductDragCmd extends Command {
/**
* 相邻元素
*/
@Schema(title = "相邻元素前")
private Long prevId;
/**
* 相邻元素
*/
@Schema(title = "相邻元素后")
private Long nextId;
/**
* 当前元素
*/
@Schema(title = "当前元素")
private Long currentId;
}

View File

@ -0,0 +1,18 @@
package com.xunhong.erp.turbo.api.biz.dto.cmd;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品表更新")
@EqualsAndHashCode(callSuper = true)
public class ProductUpdateCmd extends ProductCreateCmd {
@Schema(title = "产品表ID", requiredMode = Schema.RequiredMode.REQUIRED, type = "string")
private Long productId;
}

View File

@ -0,0 +1,19 @@
package com.xunhong.erp.turbo.api.biz.dto.qry;
import com.xunhong.erp.turbo.base.dto.Query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品表列表查询")
@EqualsAndHashCode(callSuper = true)
public class ProductListQry extends Query {
@Schema(title = "产品表ID", type = "string")
private Long productId;
}

View File

@ -0,0 +1,19 @@
package com.xunhong.erp.turbo.api.biz.dto.qry;
import com.xunhong.erp.turbo.base.dto.PageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品表分页查询")
@EqualsAndHashCode(callSuper = true)
public class ProductPageQry extends PageQuery {
@Schema(title = "产品表ID", type = "string")
private Long productId;
}

View File

@ -0,0 +1,19 @@
package com.xunhong.erp.turbo.api.biz.dto.qry;
import com.xunhong.erp.turbo.base.dto.Query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品表查询")
@EqualsAndHashCode(callSuper = true)
public class ProductShowQry extends Query {
@Schema(title = "产品表ID", type = "string")
private Long productId;
}

View File

@ -0,0 +1,69 @@
package com.xunhong.erp.turbo.api.biz.dto.vo;
import com.alibaba.cola.dto.DTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.*;
import java.time.*;
import java.util.List;
/**
* @author shenyifei
*/
@Data
@Schema(title = "产品表")
@EqualsAndHashCode(callSuper = true)
public class ProductVO extends DTO {
/**
* 产品ID
*/
@Schema(title = "产品ID", type = "string", requiredMode = Schema.RequiredMode.REQUIRED)
private Long productId;
/**
* 产品名称
*/
@Schema(title = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
/**
* 关联成本费用id
*/
@Schema(title = "关联成本费用id")
private List<Long> costItemIds;
/**
* 备注
*/
@Schema(title = "备注")
private String remark;
/**
* 排序号
*/
@Schema(title = "排序号", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal sort;
/**
* 状态1_启用0_禁用
*/
@Schema(title = "状态1_启用0_禁用", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean status;
/**
* 成本费用
*/
@Schema(title = "成本费用")
private List<CostItemVO> costItemVOList;
/**
* 创建时间
*/
@Schema(title = "创建时间")
private LocalDateTime createdAt;
}