feat: 补充平台端库存管理模块

补齐平台端库存余额、流水、初始化和手工调整能力,并将快递发货接入库存扣减闭环,方便运营侧统一查账与审计。

Made-with: Cursor
This commit is contained in:
AriadenCaseblg
2026-04-19 23:43:46 +08:00
parent b097837aa3
commit 005bd968df
32 changed files with 61113 additions and 59368 deletions

View File

@@ -18,4 +18,12 @@
- **已修复**商户订单页面order/merchantList点击详情等按钮提示“订单不存在”
- **已修复**商户订单页面order/merchantList不显示订单数据
- **已修复**平台管理后台商户订单打印页(/order/merchantPrint中的收货信息中电话显示全部电话号码不使用maskedUserPhone
- **已修复**平台管理后台商户订单打印页(/order/merchantPrint中的收货信息中电话显示全部电话号码不使用maskedUserPhone
- **已修复**商户订单页面order/merchantList测试发货功能
```
订单发送货弹窗中快递公司下拉列表没有数据
```
```
```

View File

@@ -0,0 +1,24 @@
# 库存管理
- 复制PRO_v3.0.1目录下项目中的库存管理模块功能
## 当前实现口径
- 平台端已新增库存管理、库存流水、库存初始化、手工入库/出库能力
- 库存余额表使用 `eb_product_inventory`,库存流水表使用 `eb_product_inventory_record`
- 平台商户订单在快递发货、拆单快递发货成功后,会在同一事务内校验库存余额、扣减余额并写入出库流水
- 无需发货、商家配送等非快递发货场景本期不自动出库,保持与一期范围一致
- 库存初始化会以现有商品/规格库存同步库存余额,并继承商户预警库存;商户未配置时使用默认预警值
- 退款退货回补、取消发货冲销仍为二期扩展项,底层流水与服务入口已预留可扩展空间
## 功能迁移和复制
- 让平台端后台具备和PRO_v3.0.1一样的库存管理功能
## 注意事项
- 不改变当前项目的代码,只通过数据库字段扩张来做数据关联
- 平台端商户订单页面order/merchantList点击发货时根据销售订单自动生成对应的出库单
## 相关文档
- https://doc.crmeb.com/pro_s/prov40/34832

View File

@@ -0,0 +1,75 @@
package com.zbkj.admin.controller.platform;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.zbkj.common.annotation.LogControllerAnnotation;
import com.zbkj.common.enums.MethodType;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.request.InventoryAdjustRequest;
import com.zbkj.common.request.InventoryInitRequest;
import com.zbkj.common.request.InventorySearchRequest;
import com.zbkj.common.response.InventoryPageResponse;
import com.zbkj.common.response.InventoryRecordPageResponse;
import com.zbkj.common.result.CommonResult;
import com.zbkj.common.utils.SecurityUtil;
import com.zbkj.service.service.InventoryInitService;
import com.zbkj.service.service.ProductInventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 平台端库存控制器
*/
@Slf4j
@RestController
@RequestMapping("api/admin/platform/inventory")
@Api(tags = "平台端库存控制器")
@ApiSupport(order = 101)
public class PlatformInventoryController {
@Resource
private ProductInventoryService productInventoryService;
@Resource
private InventoryInitService inventoryInitService;
@PreAuthorize("hasAuthority('platform:inventory:page:list')")
@ApiOperation(value = "库存分页列表")
@GetMapping("/list")
public CommonResult<CommonPage<InventoryPageResponse>> getList(@Validated InventorySearchRequest request) {
return CommonResult.success(CommonPage.restPage(productInventoryService.getPlatformPage(request)));
}
@PreAuthorize("hasAuthority('platform:inventory:record:page:list')")
@ApiOperation(value = "库存流水分页列表")
@GetMapping("/record/list")
public CommonResult<CommonPage<InventoryRecordPageResponse>> getRecordList(@Validated InventorySearchRequest request) {
return CommonResult.success(CommonPage.restPage(productInventoryService.getRecordPage(request)));
}
@PreAuthorize("hasAuthority('platform:inventory:adjust')")
@LogControllerAnnotation(intoDB = true, methodType = MethodType.UPDATE, description = "平台端库存调整")
@ApiOperation(value = "库存调整")
@PostMapping("/adjust")
public CommonResult<String> adjust(@RequestBody @Validated InventoryAdjustRequest request) {
if (productInventoryService.adjust(request, SecurityUtil.getLoginUserVo().getUser())) {
return CommonResult.success();
}
return CommonResult.failed();
}
@PreAuthorize("hasAuthority('platform:inventory:init')")
@LogControllerAnnotation(intoDB = true, methodType = MethodType.UPDATE, description = "平台端库存初始化")
@ApiOperation(value = "库存初始化")
@PostMapping("/init")
public CommonResult<String> init(@RequestBody InventoryInitRequest request) {
if (inventoryInitService.initInventory(request, SecurityUtil.getLoginUserVo().getUser())) {
return CommonResult.success();
}
return CommonResult.failed();
}
}

View File

@@ -0,0 +1,26 @@
package com.zbkj.common.enums;
/**
* 库存方向枚举
*/
public enum InventoryPmEnum {
IN(1, "入库"),
OUT(0, "出库");
private final Integer code;
private final String name;
InventoryPmEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
public Integer getCode() {
return code;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,28 @@
package com.zbkj.common.enums;
/**
* 库存来源类型
*/
public enum InventorySourceTypeEnum {
MANUAL_IN("manual_in", "手工入库"),
MANUAL_OUT("manual_out", "手工出库"),
ORDER_DELIVERY("order_delivery", "订单发货出库"),
INIT_SYNC("init_sync", "库存初始化");
private final String code;
private final String name;
InventorySourceTypeEnum(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,71 @@
package com.zbkj.common.model.product;
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 商品库存余额表
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("eb_product_inventory")
@ApiModel(value = "ProductInventory对象", description = "商品库存余额表")
public class ProductInventory implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "商户ID")
private Integer merId;
@ApiModelProperty(value = "商品ID")
private Integer productId;
@ApiModelProperty(value = "商品规格值ID")
private Integer attrValueId;
@ApiModelProperty(value = "商品sku")
private String sku;
@ApiModelProperty(value = "商品名称快照")
private String productName;
@ApiModelProperty(value = "商品图片快照")
private String image;
@ApiModelProperty(value = "当前可用库存")
private Integer stock;
@ApiModelProperty(value = "预警库存")
private Integer alertStock;
@ApiModelProperty(value = "最后一次操作时间")
private Date lastOperateTime;
@ApiModelProperty(value = "是否删除")
private Boolean isDel;
@ApiModelProperty(value = "创建时间")
private Date createTime;
@ApiModelProperty(value = "更新时间")
private Date updateTime;
@ApiModelProperty(value = "商户名称")
@TableField(exist = false)
private String merchantName;
}

View File

@@ -0,0 +1,96 @@
package com.zbkj.common.model.product;
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 商品库存流水表
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("eb_product_inventory_record")
@ApiModel(value = "ProductInventoryRecord对象", description = "商品库存流水表")
public class ProductInventoryRecord implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "库存ID")
private Integer inventoryId;
@ApiModelProperty(value = "商户ID")
private Integer merId;
@ApiModelProperty(value = "商品ID")
private Integer productId;
@ApiModelProperty(value = "商品规格值ID")
private Integer attrValueId;
@ApiModelProperty(value = "商品sku")
private String sku;
@ApiModelProperty(value = "商品名称快照")
private String productName;
@ApiModelProperty(value = "方向1入库 0出库")
private Integer pm;
@ApiModelProperty(value = "变动数量")
private Integer number;
@ApiModelProperty(value = "变动前库存")
private Integer beforeStock;
@ApiModelProperty(value = "变动后库存")
private Integer afterStock;
@ApiModelProperty(value = "成本价")
private BigDecimal costPrice;
@ApiModelProperty(value = "来源类型")
private String sourceType;
@ApiModelProperty(value = "来源单号")
private String sourceNo;
@ApiModelProperty(value = "来源关联ID")
private Integer sourceId;
@ApiModelProperty(value = "来源详情ID")
private Integer sourceDetailId;
@ApiModelProperty(value = "操作人ID")
private Integer operateAdminId;
@ApiModelProperty(value = "操作人类型")
private Integer operateAdminType;
@ApiModelProperty(value = "操作人名称")
private String operateAdminName;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "创建时间")
private Date createTime;
@ApiModelProperty(value = "商户名称")
@TableField(exist = false)
private String merchantName;
}

View File

@@ -0,0 +1,47 @@
package com.zbkj.common.request;
import com.zbkj.common.annotation.StringContains;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 库存调整请求对象
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "InventoryAdjustRequest对象", description = "库存调整请求对象")
public class InventoryAdjustRequest implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "商品ID", required = true)
@NotNull(message = "商品ID不能为空")
private Integer productId;
@ApiModelProperty(value = "商品规格值ID")
private Integer attrValueId;
@ApiModelProperty(value = "调整类型manual_in/manual_out", required = true)
@NotBlank(message = "调整类型不能为空")
@StringContains(limitValues = {"manual_in", "manual_out"}, message = "未知的库存调整类型")
private String sourceType;
@ApiModelProperty(value = "调整数量", required = true)
@NotNull(message = "调整数量不能为空")
@Min(value = 1, message = "调整数量必须大于0")
private Integer number;
@ApiModelProperty(value = "备注")
@Length(max = 255, message = "备注最多255个字符")
private String remark;
}

View File

@@ -0,0 +1,27 @@
package com.zbkj.common.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 库存初始化请求对象
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "InventoryInitRequest对象", description = "库存初始化请求对象")
public class InventoryInitRequest implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "商户ID不传表示全量")
private Integer merId;
@ApiModelProperty(value = "是否重建已存在库存记录")
private Boolean rebuild = false;
}

View File

@@ -0,0 +1,36 @@
package com.zbkj.common.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 库存搜索请求对象
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "InventorySearchRequest对象", description = "库存搜索请求对象")
public class InventorySearchRequest extends PageParamRequest implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "商户ID")
private Integer merId;
@ApiModelProperty(value = "商品ID")
private Integer productId;
@ApiModelProperty(value = "商品名称/sku关键词")
private String keywords;
@ApiModelProperty(value = "是否仅预警库存")
private Boolean alertOnly;
@ApiModelProperty(value = "来源类型")
private String sourceType;
}

View File

@@ -0,0 +1,55 @@
package com.zbkj.common.response;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 库存分页响应对象
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "InventoryPageResponse对象", description = "库存分页响应对象")
public class InventoryPageResponse implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "库存ID")
private Integer id;
@ApiModelProperty(value = "商户ID")
private Integer merId;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "商品ID")
private Integer productId;
@ApiModelProperty(value = "商品名称")
private String productName;
@ApiModelProperty(value = "商品图片")
private String image;
@ApiModelProperty(value = "商品规格值ID")
private Integer attrValueId;
@ApiModelProperty(value = "商品sku")
private String sku;
@ApiModelProperty(value = "当前库存")
private Integer stock;
@ApiModelProperty(value = "预警库存")
private Integer alertStock;
@ApiModelProperty(value = "最近操作时间")
private Date lastOperateTime;
}

View File

@@ -0,0 +1,89 @@
package com.zbkj.common.response;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 库存流水分页响应对象
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "InventoryRecordPageResponse对象", description = "库存流水分页响应对象")
public class InventoryRecordPageResponse implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "流水ID")
private Integer id;
@ApiModelProperty(value = "库存ID")
private Integer inventoryId;
@ApiModelProperty(value = "商户ID")
private Integer merId;
@ApiModelProperty(value = "商户名称")
private String merchantName;
@ApiModelProperty(value = "商品ID")
private Integer productId;
@ApiModelProperty(value = "商品名称")
private String productName;
@ApiModelProperty(value = "商品规格值ID")
private Integer attrValueId;
@ApiModelProperty(value = "商品sku")
private String sku;
@ApiModelProperty(value = "方向")
private Integer pm;
@ApiModelProperty(value = "数量")
private Integer number;
@ApiModelProperty(value = "变更前库存")
private Integer beforeStock;
@ApiModelProperty(value = "变更后库存")
private Integer afterStock;
@ApiModelProperty(value = "成本价")
private BigDecimal costPrice;
@ApiModelProperty(value = "来源类型")
private String sourceType;
@ApiModelProperty(value = "来源单号")
private String sourceNo;
@ApiModelProperty(value = "来源ID")
private Integer sourceId;
@ApiModelProperty(value = "来源详情ID")
private Integer sourceDetailId;
@ApiModelProperty(value = "操作人ID")
private Integer operateAdminId;
@ApiModelProperty(value = "操作人类型")
private Integer operateAdminType;
@ApiModelProperty(value = "操作人名称")
private String operateAdminName;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "创建时间")
private Date createTime;
}

View File

@@ -0,0 +1,10 @@
package com.zbkj.service.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zbkj.common.model.product.ProductInventory;
/**
* 商品库存余额 Mapper
*/
public interface ProductInventoryDao extends BaseMapper<ProductInventory> {
}

View File

@@ -0,0 +1,10 @@
package com.zbkj.service.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zbkj.common.model.product.ProductInventoryRecord;
/**
* 商品库存流水 Mapper
*/
public interface ProductInventoryRecordDao extends BaseMapper<ProductInventoryRecord> {
}

View File

@@ -0,0 +1,12 @@
package com.zbkj.service.service;
import com.zbkj.common.model.admin.SystemAdmin;
import com.zbkj.common.request.InventoryInitRequest;
/**
* 库存初始化服务
*/
public interface InventoryInitService {
Boolean initInventory(InventoryInitRequest request, SystemAdmin admin);
}

View File

@@ -0,0 +1,43 @@
package com.zbkj.service.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.github.pagehelper.PageInfo;
import com.zbkj.common.model.admin.SystemAdmin;
import com.zbkj.common.model.order.OrderInvoiceDetail;
import com.zbkj.common.model.product.ProductInventory;
import com.zbkj.common.request.InventoryAdjustRequest;
import com.zbkj.common.request.InventoryInitRequest;
import com.zbkj.common.request.InventorySearchRequest;
import com.zbkj.common.response.InventoryPageResponse;
import com.zbkj.common.response.InventoryRecordPageResponse;
import java.util.List;
/**
* 商品库存服务
*/
public interface ProductInventoryService extends IService<ProductInventory> {
/**
* 默认预警库存
*/
Integer DEFAULT_ALERT_STOCK = 10;
PageInfo<InventoryPageResponse> getPlatformPage(InventorySearchRequest request);
PageInfo<InventoryRecordPageResponse> getRecordPage(InventorySearchRequest request);
Boolean adjust(InventoryAdjustRequest request, SystemAdmin admin);
Boolean initInventory(InventoryInitRequest request, SystemAdmin admin);
/**
* 快递发货时同步扣减库存余额并记录流水。
*/
Boolean recordOrderDelivery(String orderNo, Integer invoiceId, List<OrderInvoiceDetail> invoiceDetailList, SystemAdmin admin);
/**
* 二期逆向库存扩展点:退款回补、取消发货冲销可复用此入口。
*/
Boolean reverseOrderDelivery(String orderNo, Integer invoiceId, List<OrderInvoiceDetail> invoiceDetailList, SystemAdmin admin, String remark);
}

View File

@@ -0,0 +1,24 @@
package com.zbkj.service.service.impl;
import com.zbkj.common.model.admin.SystemAdmin;
import com.zbkj.common.request.InventoryInitRequest;
import com.zbkj.service.service.InventoryInitService;
import com.zbkj.service.service.ProductInventoryService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 库存初始化服务实现
*/
@Service
public class InventoryInitServiceImpl implements InventoryInitService {
@Resource
private ProductInventoryService productInventoryService;
@Override
public Boolean initInventory(InventoryInitRequest request, SystemAdmin admin) {
return productInventoryService.initInventory(request, admin);
}
}

View File

@@ -0,0 +1,356 @@
package com.zbkj.service.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.zbkj.common.constants.Constants;
import com.zbkj.common.enums.InventoryPmEnum;
import com.zbkj.common.enums.InventorySourceTypeEnum;
import com.zbkj.common.exception.CrmebException;
import com.zbkj.common.model.admin.SystemAdmin;
import com.zbkj.common.model.merchant.Merchant;
import com.zbkj.common.model.merchant.MerchantInfo;
import com.zbkj.common.model.order.OrderInvoiceDetail;
import com.zbkj.common.model.product.Product;
import com.zbkj.common.model.product.ProductAttrValue;
import com.zbkj.common.model.product.ProductInventory;
import com.zbkj.common.model.product.ProductInventoryRecord;
import com.zbkj.common.request.InventoryAdjustRequest;
import com.zbkj.common.request.InventoryInitRequest;
import com.zbkj.common.request.InventorySearchRequest;
import com.zbkj.common.response.InventoryPageResponse;
import com.zbkj.common.response.InventoryRecordPageResponse;
import com.zbkj.service.dao.ProductInventoryDao;
import com.zbkj.service.dao.ProductInventoryRecordDao;
import com.zbkj.service.service.MerchantInfoService;
import com.zbkj.service.service.MerchantService;
import com.zbkj.service.service.ProductAttrValueService;
import com.zbkj.service.service.ProductInventoryService;
import com.zbkj.service.service.ProductService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 商品库存服务实现
*/
@Service
public class ProductInventoryServiceImpl extends ServiceImpl<ProductInventoryDao, ProductInventory> implements ProductInventoryService {
@Resource
private ProductInventoryDao dao;
@Resource
private ProductInventoryRecordDao productInventoryRecordDao;
@Resource
private ProductService productService;
@Resource
private ProductAttrValueService productAttrValueService;
@Resource
private MerchantService merchantService;
@Resource
private MerchantInfoService merchantInfoService;
@Resource
private TransactionTemplate transactionTemplate;
@Override
public PageInfo<InventoryPageResponse> getPlatformPage(InventorySearchRequest request) {
LambdaQueryWrapper<ProductInventory> lqw = Wrappers.lambdaQuery();
lqw.eq(ProductInventory::getIsDel, false);
if (ObjectUtil.isNotNull(request.getMerId()) && request.getMerId() > 0) {
lqw.eq(ProductInventory::getMerId, request.getMerId());
}
if (ObjectUtil.isNotNull(request.getProductId()) && request.getProductId() > 0) {
lqw.eq(ProductInventory::getProductId, request.getProductId());
}
if (Boolean.TRUE.equals(request.getAlertOnly())) {
lqw.apply("stock <= alert_stock");
}
if (StrUtil.isNotBlank(request.getKeywords())) {
String keywords = request.getKeywords().trim();
lqw.and(wrapper -> wrapper.like(ProductInventory::getProductName, keywords)
.or().like(ProductInventory::getSku, keywords));
}
lqw.orderByDesc(ProductInventory::getLastOperateTime, ProductInventory::getId);
Page<ProductInventory> page = PageHelper.startPage(request.getPage(), request.getLimit());
List<ProductInventory> inventoryList = dao.selectList(lqw);
if (CollUtil.isEmpty(inventoryList)) {
return com.zbkj.common.page.CommonPage.copyPageInfo(page, new ArrayList<>());
}
Map<Integer, Merchant> merchantMap = merchantService.getMapByIdList(inventoryList.stream()
.map(ProductInventory::getMerId).filter(Objects::nonNull).distinct().collect(Collectors.toList()));
List<InventoryPageResponse> responseList = inventoryList.stream().map(item -> {
InventoryPageResponse response = new InventoryPageResponse();
BeanUtils.copyProperties(item, response);
Merchant merchant = merchantMap.get(item.getMerId());
response.setMerchantName(ObjectUtil.isNotNull(merchant) ? merchant.getName() : "");
return response;
}).collect(Collectors.toList());
return com.zbkj.common.page.CommonPage.copyPageInfo(page, responseList);
}
@Override
public PageInfo<InventoryRecordPageResponse> getRecordPage(InventorySearchRequest request) {
LambdaQueryWrapper<ProductInventoryRecord> lqw = Wrappers.lambdaQuery();
if (ObjectUtil.isNotNull(request.getMerId()) && request.getMerId() > 0) {
lqw.eq(ProductInventoryRecord::getMerId, request.getMerId());
}
if (ObjectUtil.isNotNull(request.getProductId()) && request.getProductId() > 0) {
lqw.eq(ProductInventoryRecord::getProductId, request.getProductId());
}
if (StrUtil.isNotBlank(request.getSourceType())) {
lqw.eq(ProductInventoryRecord::getSourceType, request.getSourceType());
}
if (StrUtil.isNotBlank(request.getKeywords())) {
String keywords = request.getKeywords().trim();
lqw.and(wrapper -> wrapper.like(ProductInventoryRecord::getProductName, keywords)
.or().like(ProductInventoryRecord::getSku, keywords)
.or().like(ProductInventoryRecord::getSourceNo, keywords));
}
lqw.orderByDesc(ProductInventoryRecord::getId);
Page<ProductInventoryRecord> page = PageHelper.startPage(request.getPage(), request.getLimit());
List<ProductInventoryRecord> recordList = productInventoryRecordDao.selectList(lqw);
if (CollUtil.isEmpty(recordList)) {
return com.zbkj.common.page.CommonPage.copyPageInfo(page, new ArrayList<>());
}
Map<Integer, Merchant> merchantMap = merchantService.getMapByIdList(recordList.stream()
.map(ProductInventoryRecord::getMerId).filter(Objects::nonNull).distinct().collect(Collectors.toList()));
List<InventoryRecordPageResponse> responseList = recordList.stream().map(item -> {
InventoryRecordPageResponse response = new InventoryRecordPageResponse();
BeanUtils.copyProperties(item, response);
Merchant merchant = merchantMap.get(item.getMerId());
response.setMerchantName(ObjectUtil.isNotNull(merchant) ? merchant.getName() : "");
return response;
}).collect(Collectors.toList());
return com.zbkj.common.page.CommonPage.copyPageInfo(page, responseList);
}
@Override
public Boolean adjust(InventoryAdjustRequest request, SystemAdmin admin) {
Product product = productService.getById(request.getProductId());
if (ObjectUtil.isNull(product) || Boolean.TRUE.equals(product.getIsDel())) {
throw new CrmebException("商品不存在");
}
ProductAttrValue attrValue = resolveAttrValue(product, request.getAttrValueId());
Integer beforeStock = getAvailableStock(product, attrValue);
Date now = DateUtil.date();
Boolean isIn = InventorySourceTypeEnum.MANUAL_IN.getCode().equals(request.getSourceType());
if (!isIn && request.getNumber() > beforeStock) {
throw new CrmebException("库存不足,无法出库");
}
Boolean execute = transactionTemplate.execute(e -> {
if (isIn) {
productService.operationStock(product.getId(), request.getNumber(), Constants.OPERATION_TYPE_QUICK_ADD);
if (ObjectUtil.isNotNull(attrValue)) {
productAttrValueService.operationStock(attrValue.getId(), request.getNumber(), Constants.OPERATION_TYPE_QUICK_ADD,
product.getType(), product.getMarketingType(), attrValue.getVersion());
}
} else {
productService.operationStock(product.getId(), request.getNumber(), Constants.OPERATION_TYPE_DELETE);
if (ObjectUtil.isNotNull(attrValue)) {
productAttrValueService.operationStock(attrValue.getId(), request.getNumber(), Constants.OPERATION_TYPE_DELETE,
product.getType(), product.getMarketingType(), attrValue.getVersion());
}
}
Product freshProduct = productService.getById(product.getId());
ProductAttrValue freshAttrValue = ObjectUtil.isNotNull(attrValue) ? productAttrValueService.getById(attrValue.getId()) : null;
Integer afterStock = ObjectUtil.isNotNull(freshAttrValue) ? freshAttrValue.getStock() : freshProduct.getStock();
ProductInventory inventory = upsertInventory(freshProduct, freshAttrValue, afterStock, now);
saveRecord(inventory, freshProduct, freshAttrValue, isIn ? InventoryPmEnum.IN : InventoryPmEnum.OUT, request.getNumber(),
beforeStock, afterStock, request.getSourceType(), "", null, null, admin, request.getRemark(), now);
return Boolean.TRUE;
});
return Boolean.TRUE.equals(execute);
}
@Override
public Boolean initInventory(InventoryInitRequest request, SystemAdmin admin) {
LambdaQueryWrapper<Product> productWrapper = Wrappers.lambdaQuery();
productWrapper.eq(Product::getIsDel, false);
productWrapper.eq(Product::getMarketingType, 0);
if (ObjectUtil.isNotNull(request.getMerId()) && request.getMerId() > 0) {
productWrapper.eq(Product::getMerId, request.getMerId());
}
List<Product> productList = productService.list(productWrapper);
Date now = DateUtil.date();
for (Product product : productList) {
List<ProductAttrValue> attrValueList = productAttrValueService.getListByProductIdAndType(product.getId(),
product.getType(), product.getMarketingType(), false);
if (CollUtil.isEmpty(attrValueList)) {
syncInventorySnapshot(product, null, admin, request.getRebuild(), now);
continue;
}
for (ProductAttrValue attrValue : attrValueList) {
syncInventorySnapshot(product, attrValue, admin, request.getRebuild(), now);
}
}
return true;
}
@Override
public Boolean recordOrderDelivery(String orderNo, Integer invoiceId, List<OrderInvoiceDetail> invoiceDetailList, SystemAdmin admin) {
if (CollUtil.isEmpty(invoiceDetailList)) {
return true;
}
Date now = DateUtil.date();
invoiceDetailList.forEach(detail -> {
Product product = productService.getById(detail.getProductId());
if (ObjectUtil.isNull(product) || Boolean.TRUE.equals(product.getIsDel())) {
return;
}
ProductAttrValue attrValue = ObjectUtil.isNotNull(detail.getAttrValueId()) && detail.getAttrValueId() > 0
? productAttrValueService.getById(detail.getAttrValueId()) : null;
ProductInventory inventory = getOrInitInventory(product, attrValue, now);
Integer beforeStock = ObjectUtil.isNull(inventory.getStock()) ? 0 : inventory.getStock();
if (detail.getNum() > beforeStock) {
throw new CrmebException(StrUtil.format("商品【{}】库存不足,无法发货", product.getName()));
}
Integer afterStock = beforeStock - detail.getNum();
inventory.setStock(afterStock);
inventory.setLastOperateTime(now);
inventory.setUpdateTime(now);
saveOrUpdate(inventory);
saveRecord(inventory, product, attrValue, InventoryPmEnum.OUT, detail.getNum(), beforeStock, afterStock,
InventorySourceTypeEnum.ORDER_DELIVERY.getCode(), orderNo, invoiceId, detail.getId(), admin,
"订单发货出库", now);
});
return true;
}
@Override
public Boolean reverseOrderDelivery(String orderNo, Integer invoiceId, List<OrderInvoiceDetail> invoiceDetailList, SystemAdmin admin, String remark) {
// 一期先预留统一逆向入口,后续退款回补/取消发货按同一库存模型接入。
throw new CrmebException("逆向库存能力将在二期开放");
}
private void syncInventorySnapshot(Product product, ProductAttrValue attrValue, SystemAdmin admin, Boolean rebuild, Date now) {
Integer stock = ObjectUtil.isNotNull(attrValue) ? attrValue.getStock() : product.getStock();
ProductInventory exist = getByUniqueKey(product.getMerId(), product.getId(), ObjectUtil.isNotNull(attrValue) ? attrValue.getId() : 0);
Integer beforeStock = ObjectUtil.isNotNull(exist) ? exist.getStock() : 0;
ProductInventory inventory = upsertInventory(product, attrValue, stock, now);
if (ObjectUtil.isNull(exist) || Boolean.TRUE.equals(rebuild) || !Objects.equals(beforeStock, stock)) {
saveRecord(inventory, product, attrValue, InventoryPmEnum.IN, stock, beforeStock, stock,
InventorySourceTypeEnum.INIT_SYNC.getCode(), "", null, null, admin, "库存初始化同步", now);
}
}
private ProductAttrValue resolveAttrValue(Product product, Integer attrValueId) {
if (ObjectUtil.isNull(attrValueId) || attrValueId <= 0) {
if (!Boolean.TRUE.equals(product.getSpecType())) {
return null;
}
List<ProductAttrValue> attrValueList = productAttrValueService.getListByProductIdAndType(product.getId(),
product.getType(), product.getMarketingType(), false);
if (attrValueList.size() == 1) {
return attrValueList.get(0);
}
throw new CrmebException("多规格商品请选择具体规格");
}
ProductAttrValue attrValue = productAttrValueService.getById(attrValueId);
if (ObjectUtil.isNull(attrValue) || !attrValue.getProductId().equals(product.getId()) || Boolean.TRUE.equals(attrValue.getIsDel())) {
throw new CrmebException("商品规格不存在");
}
return attrValue;
}
private ProductInventory upsertInventory(Product product, ProductAttrValue attrValue, Integer stock, Date now) {
Integer attrValueId = ObjectUtil.isNotNull(attrValue) ? attrValue.getId() : 0;
ProductInventory inventory = getByUniqueKey(product.getMerId(), product.getId(), attrValueId);
if (ObjectUtil.isNull(inventory)) {
inventory = new ProductInventory();
inventory.setMerId(product.getMerId());
inventory.setProductId(product.getId());
inventory.setAttrValueId(attrValueId);
inventory.setIsDel(false);
inventory.setCreateTime(now);
}
if (ObjectUtil.isNull(inventory.getAlertStock())) {
inventory.setAlertStock(resolveAlertStock(product.getMerId()));
}
inventory.setSku(ObjectUtil.isNotNull(attrValue) ? attrValue.getSku() : "");
inventory.setProductName(product.getName());
inventory.setImage(ObjectUtil.isNotNull(attrValue) && StrUtil.isNotBlank(attrValue.getImage()) ? attrValue.getImage() : product.getImage());
inventory.setStock(stock);
inventory.setLastOperateTime(now);
inventory.setUpdateTime(now);
saveOrUpdate(inventory);
return inventory;
}
private ProductInventory getByUniqueKey(Integer merId, Integer productId, Integer attrValueId) {
LambdaQueryWrapper<ProductInventory> lqw = Wrappers.lambdaQuery();
lqw.eq(ProductInventory::getMerId, merId);
lqw.eq(ProductInventory::getProductId, productId);
lqw.eq(ProductInventory::getAttrValueId, attrValueId);
lqw.eq(ProductInventory::getIsDel, false);
lqw.last(" limit 1");
return dao.selectOne(lqw);
}
private Integer getAvailableStock(Product product, ProductAttrValue attrValue) {
ProductInventory inventory = getByUniqueKey(product.getMerId(), product.getId(), ObjectUtil.isNotNull(attrValue) ? attrValue.getId() : 0);
if (ObjectUtil.isNotNull(inventory) && ObjectUtil.isNotNull(inventory.getStock())) {
return inventory.getStock();
}
return ObjectUtil.isNotNull(attrValue) ? attrValue.getStock() : product.getStock();
}
private ProductInventory getOrInitInventory(Product product, ProductAttrValue attrValue, Date now) {
Integer attrValueId = ObjectUtil.isNotNull(attrValue) ? attrValue.getId() : 0;
ProductInventory inventory = getByUniqueKey(product.getMerId(), product.getId(), attrValueId);
if (ObjectUtil.isNotNull(inventory)) {
return inventory;
}
Integer currentStock = ObjectUtil.isNotNull(attrValue) ? attrValue.getStock() : product.getStock();
return upsertInventory(product, attrValue, currentStock, now);
}
private Integer resolveAlertStock(Integer merId) {
MerchantInfo merchantInfo = merchantInfoService.getByMerId(merId);
if (ObjectUtil.isNotNull(merchantInfo) && ObjectUtil.isNotNull(merchantInfo.getAlertStock()) && merchantInfo.getAlertStock() >= 0) {
return merchantInfo.getAlertStock();
}
return ProductInventoryService.DEFAULT_ALERT_STOCK;
}
private void saveRecord(ProductInventory inventory, Product product, ProductAttrValue attrValue, InventoryPmEnum pmEnum, Integer number,
Integer beforeStock, Integer afterStock, String sourceType, String sourceNo, Integer sourceId,
Integer sourceDetailId, SystemAdmin admin, String remark, Date now) {
ProductInventoryRecord record = new ProductInventoryRecord();
record.setInventoryId(inventory.getId());
record.setMerId(product.getMerId());
record.setProductId(product.getId());
record.setAttrValueId(ObjectUtil.isNotNull(attrValue) ? attrValue.getId() : 0);
record.setSku(ObjectUtil.isNotNull(attrValue) ? attrValue.getSku() : "");
record.setProductName(product.getName());
record.setPm(pmEnum.getCode());
record.setNumber(number);
record.setBeforeStock(beforeStock);
record.setAfterStock(afterStock);
record.setCostPrice(ObjectUtil.isNotNull(attrValue) ? attrValue.getCost() : product.getCost());
record.setSourceType(sourceType);
record.setSourceNo(sourceNo);
record.setSourceId(sourceId);
record.setSourceDetailId(sourceDetailId);
record.setOperateAdminId(ObjectUtil.isNotNull(admin) ? admin.getId() : 0);
record.setOperateAdminType(ObjectUtil.isNotNull(admin) ? admin.getType() : 0);
record.setOperateAdminName(ObjectUtil.isNotNull(admin) ? admin.getRealName() : "system");
record.setRemark(StrUtil.isBlank(remark) ? "" : remark);
record.setCreateTime(now);
productInventoryRecordDao.insert(record);
}
}

View File

@@ -142,9 +142,36 @@ INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1356);
-- ---------------------------------------------------------------------------
UPDATE `eb_system_role`
SET `rules` = CONCAT(`rules`, ',1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1340,1341,1342,1343,1344,1345,1346,1347,1350,1351,1352,1353,1354,1355,1356')
SET `rules` = CONCAT(`rules`, ',1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1340,1341,1342,1343,1344,1345,1346,1347,1350,1351,1352,1353,1354,1355,1356,1360,1361,1362,1363,1364,1365')
WHERE `id` = 1;
-- ============================================================================
-- 平台端库存管理菜单
-- 挂载到平台"商品"目录(pid=2)下
-- ============================================================================
INSERT INTO `eb_system_menu` (`id`, `pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `type`)
VALUES (1360, 2, '库存管理', '', '', '/product/inventory', 'C', 98, 1, 0, 3);
INSERT INTO `eb_system_menu` (`id`, `pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `type`)
VALUES (1361, 2, '库存流水', '', '', '/product/inventory/record', 'C', 97, 1, 0, 3);
INSERT INTO `eb_system_menu` (`id`, `pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `type`)
VALUES (1362, 1360, '库存分页列表', '', 'platform:inventory:page:list', '', 'A', 1, 1, 0, 3);
INSERT INTO `eb_system_menu` (`id`, `pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `type`)
VALUES (1363, 1361, '库存流水分页列表', '', 'platform:inventory:record:page:list', '', 'A', 1, 1, 0, 3);
INSERT INTO `eb_system_menu` (`id`, `pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `type`)
VALUES (1364, 1360, '库存调整', '', 'platform:inventory:adjust', '', 'A', 1, 1, 0, 3);
INSERT INTO `eb_system_menu` (`id`, `pid`, `name`, `icon`, `perms`, `component`, `menu_type`, `sort`, `is_show`, `is_delte`, `type`)
VALUES (1365, 1360, '库存初始化', '', 'platform:inventory:init', '', 'A', 1, 1, 0, 3);
INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1360);
INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1361);
INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1362);
INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1363);
INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1364);
INSERT INTO `eb_system_role_menu` (`rid`, `menu_id`) VALUES (1, 1365);
-- ---------------------------------------------------------------------------
-- 完成说明

View File

@@ -4429,9 +4429,6 @@ CREATE TABLE `wa_users` (
KEY `email` (`salt`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=93111 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户表';
-- ----------------------------
-- Table structure for wa_withdraw
-- ----------------------------
DROP TABLE IF EXISTS `wa_withdraw`;
CREATE TABLE `wa_withdraw` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
@@ -4451,4 +4448,61 @@ CREATE TABLE `wa_withdraw` (
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4948 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='提现表';
-- ----------------------------
-- Table structure for eb_product_inventory
-- ----------------------------
DROP TABLE IF EXISTS `eb_product_inventory`;
CREATE TABLE `eb_product_inventory` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mer_id` int(11) NOT NULL DEFAULT '0' COMMENT '商户ID',
`product_id` int(11) NOT NULL DEFAULT '0' COMMENT '商品ID',
`attr_value_id` int(11) NOT NULL DEFAULT '0' COMMENT '商品规格值ID单规格时为0',
`sku` varchar(128) NOT NULL DEFAULT '' COMMENT '商品sku',
`product_name` varchar(255) NOT NULL DEFAULT '' COMMENT '商品名称快照',
`image` varchar(255) NOT NULL DEFAULT '' COMMENT '商品图片快照',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '可用库存',
`alert_stock` int(11) NOT NULL DEFAULT '0' COMMENT '预警库存',
`last_operate_time` timestamp NULL DEFAULT NULL COMMENT '最后操作时间',
`is_del` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uq_mer_product_attr` (`mer_id`,`product_id`,`attr_value_id`) USING BTREE,
KEY `idx_mer_stock` (`mer_id`,`stock`) USING BTREE,
KEY `idx_product` (`product_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='商品库存余额表';
-- ----------------------------
-- Table structure for eb_product_inventory_record
-- ----------------------------
DROP TABLE IF EXISTS `eb_product_inventory_record`;
CREATE TABLE `eb_product_inventory_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`inventory_id` int(11) NOT NULL DEFAULT '0' COMMENT '库存ID',
`mer_id` int(11) NOT NULL DEFAULT '0' COMMENT '商户ID',
`product_id` int(11) NOT NULL DEFAULT '0' COMMENT '商品ID',
`attr_value_id` int(11) NOT NULL DEFAULT '0' COMMENT '商品规格值ID',
`sku` varchar(128) NOT NULL DEFAULT '' COMMENT '商品sku',
`product_name` varchar(255) NOT NULL DEFAULT '' COMMENT '商品名称快照',
`pm` tinyint(4) NOT NULL DEFAULT '1' COMMENT '方向1入库 0出库',
`number` int(11) NOT NULL DEFAULT '0' COMMENT '变动数量',
`before_stock` int(11) NOT NULL DEFAULT '0' COMMENT '变动前库存',
`after_stock` int(11) NOT NULL DEFAULT '0' COMMENT '变动后库存',
`cost_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '成本价',
`source_type` varchar(64) NOT NULL DEFAULT '' COMMENT '来源类型',
`source_no` varchar(64) NOT NULL DEFAULT '' COMMENT '来源单号',
`source_id` int(11) NOT NULL DEFAULT '0' COMMENT '来源ID',
`source_detail_id` int(11) NOT NULL DEFAULT '0' COMMENT '来源详情ID',
`operate_admin_id` int(11) NOT NULL DEFAULT '0' COMMENT '操作人ID',
`operate_admin_type` int(11) NOT NULL DEFAULT '0' COMMENT '操作人类型',
`operate_admin_name` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人名称',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_inventory` (`inventory_id`) USING BTREE,
KEY `idx_mer_product` (`mer_id`,`product_id`) USING BTREE,
KEY `idx_source_no` (`source_no`) USING BTREE,
KEY `idx_source_type` (`source_type`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='商品库存流水表';
SET FOREIGN_KEY_CHECKS = 1;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
import request from '@/utils/request'
export function inventoryListApi(params) {
return request({
url: '/admin/platform/inventory/list',
method: 'get',
params
})
}
export function inventoryRecordListApi(params) {
return request({
url: '/admin/platform/inventory/record/list',
method: 'get',
params
})
}
export function inventoryAdjustApi(data) {
return request({
url: '/admin/platform/inventory/adjust',
method: 'post',
data
})
}
export function inventoryInitApi(data) {
return request({
url: '/admin/platform/inventory/init',
method: 'post',
data
})
}

View File

@@ -63,6 +63,20 @@ export function expressList(data) {
});
}
// hooks/use-order 仍在使用 expressPageApi这里保持兼容导出
export function expressPageApi(data) {
return expressList(data);
}
// 创建物流公司弹窗会读取全部物流公司,这里补齐平台端兼容导出
export function expressAllApi(params) {
return request({
url: 'admin/express/all',
method: 'get',
params,
});
}
// 同步物流公司
export function expressSyncApi() {
return request({

View File

@@ -186,7 +186,7 @@ export function merchantOrderTimeApi(params) {
*/
export function merchantSheetInfoApi() {
return request({
url: `/admin/store/order/sheet/info`,
url: `/admin/merchant/elect/info`,
method: 'get',
});
}

View File

@@ -1,15 +1,22 @@
import { expressAllApi, expressPageApi } from '@/api/logistics';
function isEnabledExpress(item) {
if (typeof item?.isOpen === 'boolean') return item.isOpen;
if (typeof item?.isShow === 'boolean') return item.isShow;
if (typeof item?.status === 'boolean') return item.status;
return true;
}
/**
* 配置的物流公司
* @param param
* @returns {Promise<*>}
*/
// export async function useLogistics(param) {
// const res = await expressPageApi(param);
// const express = res.list.filter((item) => item.isOpen);
// return express;
// }
export async function useLogistics(param) {
const res = await expressPageApi(param);
const express = (res.list || []).filter((item) => isEnabledExpress(item));
return express;
}
/**
* 全部物流公司

View File

@@ -8,7 +8,7 @@
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import Layout from '@/layout';
import Layout from '@/layout'
const productRouter = {
path: '/product',
@@ -17,52 +17,64 @@ const productRouter = {
name: 'Product',
meta: {
title: '商品',
icon: 'clipboard',
icon: 'clipboard'
},
children: [
{
path: 'list',
component: () => import('@/views/product/index'),
name: 'ProductIndex',
meta: { title: '商品列表', icon: '' },
meta: { title: '商品列表', icon: '' }
},
{
path: 'category',
component: () => import('@/views/product/category/index'),
name: 'ProductCategory',
meta: { title: '商品分类', icon: '' },
meta: { title: '商品分类', icon: '' }
},
{
path: 'comment',
component: () => import('@/views/product/comment/index'),
name: 'ProductComment',
meta: { title: '商品评论', icon: '' },
meta: { title: '商品评论', icon: '' }
},
{
path: 'brand',
component: () => import('@/views/product/brand/index'),
name: 'ProductBrand',
meta: { title: '品牌管理', icon: '' },
meta: { title: '品牌管理', icon: '' }
},
{
path: 'guarantee',
component: () => import('@/views/product/guarantee/index'),
name: 'ProductGuarantee',
meta: { title: '保障服务', icon: '' },
meta: { title: '保障服务', icon: '' }
},
{
path: 'tag',
component: () => import('@/views/product/tag/index'),
name: 'ProductTag',
meta: { title: '商品标签', icon: '' },
meta: { title: '商品标签', icon: '' }
},
{
path: 'tag/creatTag/:id?',
component: () => import('@/views/product/tag/creatTag'),
name: 'CreatTag',
meta: { title: '添加商品标签', icon: '', noCache: true, activeMenu: `/product/tag` },
meta: { title: '添加商品标签', icon: '', noCache: true, activeMenu: `/product/tag` }
},
],
};
{
path: 'inventory',
component: () => import('@/views/inventory/index'),
name: 'ProductInventory',
meta: { title: '库存管理', icon: '' }
},
{
path: 'inventory/record',
component: () => import('@/views/inventory/record'),
name: 'ProductInventoryRecord',
meta: { title: '库存流水', icon: '', activeMenu: `/product/inventory` }
}
]
}
export default productRouter;
export default productRouter

View File

@@ -0,0 +1,115 @@
<template>
<el-dialog
:visible.sync="dialogVisible"
title="库存调整"
width="520px"
destroy-on-close
:close-on-click-modal="false"
@close="handleClose"
>
<el-form ref="form" :model="form" :rules="rules" label-width="95px" size="small" @submit.native.prevent>
<el-form-item label="商品名称">
<div>{{ currentRow.productName || '--' }}</div>
</el-form-item>
<el-form-item label="规格">
<div>{{ currentRow.sku || '默认规格' }}</div>
</el-form-item>
<el-form-item label="当前库存">
<div>{{ currentRow.stock || 0 }}</div>
</el-form-item>
<el-form-item label="调整类型" prop="sourceType">
<el-radio-group v-model="form.sourceType">
<el-radio label="manual_in">手工入库</el-radio>
<el-radio label="manual_out">手工出库</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="调整数量" prop="number">
<el-input-number v-model="form.number" :min="1" :max="999999" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model.trim="form.remark" type="textarea" :rows="3" maxlength="255" show-word-limit />
</el-form-item>
</el-form>
<span slot="footer">
<el-button size="small" @click="handleClose">取消</el-button>
<el-button size="small" type="primary" @click="handleSubmit">确定</el-button>
</span>
</el-dialog>
</template>
<script>
import { inventoryAdjustApi } from '@/api/inventory'
export default {
name: 'InventoryAdjustDialog',
props: {
visible: {
type: Boolean,
default: false
},
row: {
type: Object,
default: () => ({})
}
},
data() {
return {
dialogVisible: this.visible,
currentRow: this.row,
form: {
productId: null,
attrValueId: null,
sourceType: 'manual_in',
number: 1,
remark: ''
},
rules: {
sourceType: [{ required: true, message: '请选择调整类型', trigger: 'change' }],
number: [{ required: true, message: '请输入调整数量', trigger: 'blur' }]
}
}
},
watch: {
visible(val) {
this.dialogVisible = val
if (val) {
this.resetForm()
}
},
row: {
handler(val) {
this.currentRow = val || {}
},
deep: true
}
},
methods: {
resetForm() {
this.form = {
productId: this.currentRow.productId || null,
attrValueId: this.currentRow.attrValueId || null,
sourceType: 'manual_in',
number: 1,
remark: ''
}
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
})
},
handleClose() {
this.$emit('update:visible', false)
this.$emit('close')
},
handleSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) return
inventoryAdjustApi(this.form).then(() => {
this.$message.success('库存调整成功')
this.$emit('success')
this.handleClose()
})
})
}
}
}
</script>

View File

@@ -0,0 +1,182 @@
<template>
<div class="divBox">
<el-card shadow="never" :bordered="false" class="ivu-mt" :body-style="{ padding: 0 }">
<div class="padding-add">
<el-form inline size="small" @submit.native.prevent>
<el-form-item label="商户名称:">
<merchant-name :mer-id-checked="tableFrom.merId" @getMerId="getMerId" />
</el-form-item>
<el-form-item label="商品搜索:">
<el-input
v-model.trim="tableFrom.keywords"
placeholder="请输入商品名称或SKU"
class="selWidth"
clearable
@keyup.enter.native="handleSearchList"
/>
</el-form-item>
<el-form-item label="库存状态:">
<el-select v-model="tableFrom.alertOnly" clearable class="selWidth" @change="handleSearchList">
<el-option :value="true" label="仅预警库存" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleSearchList">查询</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
<el-button
v-hasPermi="['platform:inventory:init']"
size="small"
type="primary"
plain
@click="handleInit"
>库存初始化</el-button>
<el-button size="small" plain @click="$router.push('/product/inventory/record')">查看流水</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<el-card class="box-card mt14" :body-style="{ padding: '20px' }" shadow="never" :bordered="false">
<el-table v-loading="loading" :data="tableData.list || tableData.data || []" size="small" class="mt10">
<el-table-column prop="merchantName" label="商户名称" min-width="180" />
<el-table-column label="商品信息" min-width="260">
<template slot-scope="scope">
<div class="acea-row row-middle">
<el-image style="width: 36px; height: 36px" :src="scope.row.image" :preview-src-list="[scope.row.image]" />
<div class="ml10">
<div>{{ scope.row.productName || '--' }}</div>
<div class="font12 color-909399">SKU{{ scope.row.sku || '默认规格' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="当前库存" min-width="140">
<template slot-scope="scope">
<span>{{ scope.row.stock }}</span>
<el-tag
v-if="Number(scope.row.stock || 0) <= Number(scope.row.alertStock || 0)"
type="danger"
size="mini"
class="ml8"
>预警</el-tag>
</template>
</el-table-column>
<el-table-column prop="alertStock" label="预警库存" min-width="100" />
<el-table-column prop="lastOperateTime" label="最近变更时间" min-width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<a v-hasPermi="['platform:inventory:adjust']" @click="openAdjust(scope.row)">库存调整</a>
<el-divider direction="vertical" />
<a @click="goRecord(scope.row)">查看流水</a>
</template>
</el-table-column>
</el-table>
<div class="block">
<el-pagination
background
:page-sizes="[20, 40, 60, 80]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total || 0"
@size-change="handleSizeChange"
@current-change="pageChange"
/>
</div>
</el-card>
<adjust-dialog :visible.sync="dialogVisible" :row="currentRow" @success="getList" />
</div>
</template>
<script>
import merchantName from '@/components/merchantName/index.vue'
import AdjustDialog from './components/adjustDialog.vue'
import { inventoryInitApi, inventoryListApi } from '@/api/inventory'
export default {
name: 'InventoryIndex',
components: { merchantName, AdjustDialog },
data() {
return {
loading: false,
dialogVisible: false,
currentRow: {},
tableFrom: {
page: 1,
limit: 20,
merId: null,
keywords: '',
alertOnly: undefined
},
tableData: {
list: [],
total: 0
}
}
},
created() {
this.getList()
},
methods: {
getMerId(merId) {
this.tableFrom.merId = merId
this.handleSearchList()
},
getList() {
this.loading = true
inventoryListApi(this.tableFrom)
.then((res) => {
this.tableData = res
})
.finally(() => {
this.loading = false
})
},
handleSearchList() {
this.tableFrom.page = 1
this.getList()
},
handleReset() {
this.tableFrom = {
page: 1,
limit: 20,
merId: null,
keywords: '',
alertOnly: undefined
}
this.getList()
},
pageChange(page) {
this.tableFrom.page = page
this.getList()
},
handleSizeChange(limit) {
this.tableFrom.limit = limit
this.getList()
},
openAdjust(row) {
this.currentRow = { ...row }
this.dialogVisible = true
},
goRecord(row) {
this.$router.push({
path: '/product/inventory/record',
query: {
merId: row.merId,
productId: row.productId,
keywords: row.sku || row.productName || ''
}
})
},
handleInit() {
this.$modalSure('确认按当前商品库存重建库存余额吗?').then(() => {
inventoryInitApi({ merId: this.tableFrom.merId || null, rebuild: true }).then(() => {
this.$message.success('库存初始化完成')
this.getList()
})
})
}
}
}
</script>

View File

@@ -0,0 +1,155 @@
<template>
<div class="divBox">
<el-card shadow="never" :bordered="false" class="ivu-mt" :body-style="{ padding: 0 }">
<div class="padding-add">
<el-form inline size="small" @submit.native.prevent>
<el-form-item label="商户名称:">
<merchant-name :mer-id-checked="tableFrom.merId" @getMerId="getMerId" />
</el-form-item>
<el-form-item label="关键词:">
<el-input
v-model.trim="tableFrom.keywords"
placeholder="商品名称/SKU/来源单号"
class="selWidth"
clearable
@keyup.enter.native="handleSearchList"
/>
</el-form-item>
<el-form-item label="来源类型:">
<el-select v-model="tableFrom.sourceType" clearable class="selWidth" @change="handleSearchList">
<el-option label="手工入库" value="manual_in" />
<el-option label="手工出库" value="manual_out" />
<el-option label="订单发货出库" value="order_delivery" />
<el-option label="库存初始化" value="init_sync" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="handleSearchList">查询</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
<el-button size="small" plain @click="$router.push('/product/inventory')">返回库存</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<el-card class="box-card mt14" :body-style="{ padding: '20px' }" shadow="never" :bordered="false">
<el-table v-loading="loading" :data="tableData.list || tableData.data || []" size="small">
<el-table-column prop="merchantName" label="商户名称" min-width="160" />
<el-table-column label="商品信息" min-width="220">
<template slot-scope="scope">
<div>{{ scope.row.productName || '--' }}</div>
<div class="font12 color-909399">SKU{{ scope.row.sku || '默认规格' }}</div>
</template>
</el-table-column>
<el-table-column label="方向" min-width="90">
<template slot-scope="scope">
<span :class="scope.row.pm === 1 ? 'color-67C23A' : 'color-E93323'">
{{ scope.row.pm === 1 ? '入库' : '出库' }}
</span>
</template>
</el-table-column>
<el-table-column prop="number" label="数量" min-width="90" />
<el-table-column label="库存变化" min-width="140">
<template slot-scope="scope">{{ scope.row.beforeStock }} -> {{ scope.row.afterStock }}</template>
</el-table-column>
<el-table-column label="来源类型" min-width="130">
<template slot-scope="scope">{{ formatSourceType(scope.row.sourceType) }}</template>
</el-table-column>
<el-table-column prop="sourceNo" label="来源单号" min-width="180" />
<el-table-column prop="operateAdminName" label="操作人" min-width="120" />
<el-table-column prop="remark" label="备注" min-width="180" />
<el-table-column prop="createTime" label="创建时间" min-width="170" />
</el-table>
<div class="block">
<el-pagination
background
:page-sizes="[20, 40, 60, 80]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total || 0"
@size-change="handleSizeChange"
@current-change="pageChange"
/>
</div>
</el-card>
</div>
</template>
<script>
import merchantName from '@/components/merchantName/index.vue'
import { inventoryRecordListApi } from '@/api/inventory'
export default {
name: 'InventoryRecord',
components: { merchantName },
data() {
return {
loading: false,
tableFrom: {
page: 1,
limit: 20,
merId: this.$route.query.merId ? Number(this.$route.query.merId) : null,
productId: this.$route.query.productId ? Number(this.$route.query.productId) : null,
keywords: this.$route.query.keywords || '',
sourceType: ''
},
tableData: {
list: [],
total: 0
}
}
},
created() {
this.getList()
},
methods: {
formatSourceType(sourceType) {
const typeMap = {
manual_in: '手工入库',
manual_out: '手工出库',
order_delivery: '订单发货出库',
init_sync: '库存初始化'
}
return typeMap[sourceType] || sourceType || '--'
},
getMerId(merId) {
this.tableFrom.merId = merId
this.handleSearchList()
},
getList() {
this.loading = true
inventoryRecordListApi(this.tableFrom)
.then((res) => {
this.tableData = res
})
.finally(() => {
this.loading = false
})
},
handleSearchList() {
this.tableFrom.page = 1
this.getList()
},
handleReset() {
this.tableFrom = {
page: 1,
limit: 20,
merId: null,
productId: null,
keywords: '',
sourceType: ''
}
this.getList()
},
pageChange(page) {
this.tableFrom.page = page
this.getList()
},
handleSizeChange(limit) {
this.tableFrom.limit = limit
this.getList()
}
}
}
</script>

View File

@@ -122,9 +122,10 @@ import { defaultData } from '@/views/systemSetting/deliveryPersonnel/default';
import { personnelListApi } from '@/api/deliveryPersonnel';
import CreatPersonnel from '@/views/systemSetting/deliveryPersonnel/creatPersonnel';
import { checkPermi } from '@/utils/permission';
import { merchantElectrSheetInfo } from '@/api/systemSetting';
import { merchantSheetInfoApi as merchantElectrSheetInfo } from '@/api/merchantOrder';
import Cookies from 'js-cookie';
import { exportTempApi } from '@/api/logistics';
import { isPlatform } from '@/utils/settingMer';
export default {
name: 'sendFrom',
components: { CreatExpress, CreatPersonnel },
@@ -143,6 +144,7 @@ export default {
exportTempList: [],
merElectPrint: Cookies.get('merElectPrint'), // 商家小票打印开关状态
currentItemCode: '',
isPlatform,
};
},
props: {
@@ -157,7 +159,9 @@ export default {
},
mounted() {
this.getList();
this.getPersonnelList();
if (!this.isPlatform && this.checkPermi(['merchant:delivery:personnel:page'])) {
this.getPersonnelList();
}
//if (checkPermi(['admin:pass:shipment:express']))
this.getShipmentExpress();
},
@@ -174,15 +178,20 @@ export default {
},
// 选配送员确定回调
handlerSuccessSubmit() {
this.getPersonnelList();
if (!this.isPlatform) this.getPersonnelList();
this.dialogVisible = false;
},
// 配送员列表
async getPersonnelList() {
const data = await personnelListApi(this.tableFrom);
this.personnelList = data.list;
if (!this.isShowBtn)
try {
const data = await personnelListApi(this.tableFrom);
this.personnelList = data.list || [];
} catch (e) {
this.personnelList = [];
}
if (!this.isShowBtn) {
this.selectedValue = this.personnelList.filter((item) => item.personnelPhone === this.formItem.carrierPhone)[0];
}
},
// 添加
handleCreatPersonnel(row) {
@@ -202,17 +211,27 @@ export default {
limit: 50,
openStatus: true,
};
if (typeof useLogistics !== 'function') {
this.express = [];
return;
}
this.express = await useLogistics(params);
this.express.map((item) => {
if (item.isDefault && !this.formItem.id) this.formItem.expressCode = item.code;
});
},
getShipmentExpress() {
if (typeof merchantElectrSheetInfo !== 'function') {
this.shipmentExpress = {};
return;
}
merchantElectrSheetInfo().then((data) => {
this.shipmentExpress = data;
this.formItem.toName = data.senderUsername;
this.formItem.toTel = data.senderPhone;
this.formItem.toAddr = data.senderAddr;
this.shipmentExpress = data || {};
this.formItem.toName = data?.senderUsername || '';
this.formItem.toTel = data?.senderPhone || '';
this.formItem.toAddr = data?.senderAddr || '';
}).catch(() => {
this.shipmentExpress = {};
});
},
changeSendTypeRadio(expressRecordType) {

View File

@@ -9,10 +9,10 @@
>
<el-form v-if="modals" ref="formItem" :model="formItem" label-width="95px" @submit.native.prevent :rules="rules">
<el-form-item v-show="secondType !== OrderSecondTypeEnum.Fictitious" label="配送方式:" prop="deliveryType">
<el-radio-group v-model="formItem.deliveryType" @change="changeRadio(formItem.deliveryType)" v-removeAriaHidden>
<el-radio-group v-model="formItem.deliveryType" @change="changeRadio(formItem.deliveryType)">
<el-radio label="express">快递配送</el-radio>
<el-radio label="noNeed">无需发货</el-radio>
<el-radio label="merchant">商家送货</el-radio>
<el-radio label="merchant" v-if="!isPlatform && checkPermi(['merchant:delivery:personnel:page'])">商家送货</el-radio>
</el-radio-group>
</el-form-item>
<SendFrom :formItem="formItem" :isShowBtn="true"></SendFrom>
@@ -109,6 +109,7 @@ import SendFrom from './components/sendFrom';
import { useLogistics } from '@/hooks/use-order';
import { postRules } from '@/views/merchantOrder/default';
import { OrderSecondTypeEnum } from '@/enums/productEnums';
import { isPlatform } from '@/utils/settingMer';
const defaultObj = {
deliveryType: 'express',
isSplit: false,
@@ -169,6 +170,7 @@ export default {
},
data() {
return {
isPlatform,
OrderSecondTypeEnum: OrderSecondTypeEnum,
productList: [],
formItem: { ...defaultObj },
@@ -222,7 +224,15 @@ export default {
limit: 50,
openStatus: true,
};
this.express = await useLogistics(params);
if (typeof useLogistics !== 'function') {
this.express = [];
return;
}
try {
this.express = await useLogistics(params);
} catch (e) {
this.express = [];
}
this.express.map((item) => {
if (item.isDefault) this.formItem.expressCode = item.code;
});