feat: 补充平台端库存管理模块
补齐平台端库存余额、流水、初始化和手工调整能力,并将快递发货接入库存扣减闭环,方便运营侧统一查账与审计。 Made-with: Cursor
This commit is contained in:
@@ -18,4 +18,12 @@
|
||||
|
||||
- **已修复**商户订单页面(order/merchantList)点击详情等按钮提示“订单不存在”
|
||||
- **已修复**商户订单页面(order/merchantList),不显示订单数据,
|
||||
- **已修复**平台管理后台商户订单打印页(/order/merchantPrint)中的收货信息中电话显示全部电话号码,不使用maskedUserPhone
|
||||
- **已修复**平台管理后台商户订单打印页(/order/merchantPrint)中的收货信息中电话显示全部电话号码,不使用maskedUserPhone
|
||||
|
||||
- **已修复**商户订单页面(order/merchantList)测试发货功能:
|
||||
```
|
||||
订单发送货弹窗中快递公司下拉列表没有数据
|
||||
```
|
||||
|
||||
```
|
||||
```
|
||||
24
docs/feature-stock-0413.md
Normal file
24
docs/feature-stock-0413.md
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 完成说明
|
||||
|
||||
@@ -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;
|
||||
|
||||
112044
mer_java/sql/多商户V2.2全量.sql
112044
mer_java/sql/多商户V2.2全量.sql
File diff suppressed because one or more lines are too long
33
mer_plat_admin/src/api/inventory.js
Normal file
33
mer_plat_admin/src/api/inventory.js
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部物流公司
|
||||
|
||||
@@ -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
|
||||
|
||||
115
mer_plat_admin/src/views/inventory/components/adjustDialog.vue
Normal file
115
mer_plat_admin/src/views/inventory/components/adjustDialog.vue
Normal 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>
|
||||
182
mer_plat_admin/src/views/inventory/index.vue
Normal file
182
mer_plat_admin/src/views/inventory/index.vue
Normal 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>
|
||||
155
mer_plat_admin/src/views/inventory/record.vue
Normal file
155
mer_plat_admin/src/views/inventory/record.vue
Normal 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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user