liyuan 2 недель назад
Родитель
Сommit
b199409b50
13 измененных файлов с 847 добавлено и 41 удалено
  1. 1 1
      blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/dto/BusinessStockBillFlowDTO.java
  2. 21 0
      blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/dto/BusinessStockInventoryCheckDTO.java
  3. 5 0
      blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/entity/BusinessStockInventoryCheck.java
  4. 36 0
      blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/vo/BusinessStockInventoryCheckVO.java
  5. 94 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/controller/BusinessStockInventoryCheckController.java
  6. 12 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckDetailMapper.java
  7. 9 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckDetailMapper.xml
  8. 12 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckMapper.java
  9. 8 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckMapper.xml
  10. 44 16
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryMapper.xml
  11. 76 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/service/IBusinessStockInventoryCheckService.java
  12. 0 24
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/service/impl/BusinessStockBillServiceImpl.java
  13. 529 0
      blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/service/impl/BusinessStockInventoryCheckServiceImpl.java

+ 1 - 1
blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/dto/BusinessStockBillFlowDTO.java

@@ -26,7 +26,7 @@ public class BusinessStockBillFlowDTO implements Serializable {
 	private String customerOrSupplierName;
 
 	/**
-	 * 类型:入库/出库
+	 * 类型:入库/出库/盘点
 	 */
 	private String billTypeName;
 

+ 21 - 0
blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/dto/BusinessStockInventoryCheckDTO.java

@@ -0,0 +1,21 @@
+package com.trade.purchase.stock.dto;
+
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheck;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheckDetail;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * @author Rain
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class BusinessStockInventoryCheckDTO extends BusinessStockInventoryCheck {
+
+	/**
+	 * 盘点明细
+	 */
+	private List<BusinessStockInventoryCheckDetail> detailList;
+}

+ 5 - 0
blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/entity/BusinessStockInventoryCheck.java

@@ -107,4 +107,9 @@ public class BusinessStockInventoryCheck implements Serializable {
 	 * 是否删除(软删除):0-未删除,1-已删除
 	 */
 	private Integer isDeleted;
+
+	/**
+	 * 乐观锁
+	 */
+	private Integer version;
 }

+ 36 - 0
blade-service-api/trade-purchase-api/src/main/java/com/trade/purchase/stock/vo/BusinessStockInventoryCheckVO.java

@@ -0,0 +1,36 @@
+package com.trade.purchase.stock.vo;
+
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheck;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheckDetail;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * @author Rain
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class BusinessStockInventoryCheckVO extends BusinessStockInventoryCheck {
+
+	/**
+	 * 盘点明细
+	 */
+	private List<BusinessStockInventoryCheckDetail> detailList;
+
+	/**
+	 * 编辑时待逻辑删除的明细 ID
+	 */
+	private List<BusinessStockInventoryCheckDetail> delDetailList;
+
+	/**
+	 * 分页/导出:盘点日期起(字符串,与入库单日期筛选一致)
+	 */
+	private String checkDateStart;
+
+	/**
+	 * 分页/导出:盘点日期止
+	 */
+	private String checkDateEnd;
+}

+ 94 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/controller/BusinessStockInventoryCheckController.java

@@ -0,0 +1,94 @@
+package com.trade.purchase.stock.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.trade.purchase.annotation.VersionControl;
+import com.trade.purchase.stock.dto.BusinessStockInventoryCheckDTO;
+import com.trade.purchase.stock.service.IBusinessStockInventoryCheckService;
+import com.trade.purchase.stock.vo.BusinessStockInventoryCheckVO;
+import lombok.AllArgsConstructor;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.tool.api.R;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 库存盘点
+ * <p>
+ * 与前端约定前缀:/business/stock/inventory/check
+ * </p>
+ *
+ * @author Rain
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/business/stock/inventory/check")
+public class BusinessStockInventoryCheckController {
+
+	private final IBusinessStockInventoryCheckService inventoryCheckService;
+
+	/**
+	 * 分页列表
+	 */
+	@GetMapping("/page")
+	public R<IPage<BusinessStockInventoryCheckDTO>> page(BusinessStockInventoryCheckVO vo, Query query) {
+		return R.data(inventoryCheckService.page(vo, query));
+	}
+
+	/**
+	 * 导出(文件流,前端 axios 需 responseType: 'blob')
+	 */
+	@PostMapping("/export")
+	public void export(@RequestBody(required = false) BusinessStockInventoryCheckVO vo, HttpServletResponse response) {
+		inventoryCheckService.export(vo, response);
+	}
+
+	/**
+	 * 详情(含明细)
+	 */
+	@GetMapping("/detail")
+	public R<BusinessStockInventoryCheckDTO> detail(@RequestParam("id") Long id) {
+		return R.data(inventoryCheckService.detail(id));
+	}
+
+	/**
+	 * 保存(status=0,服务端也会以保存态处理)
+	 */
+	@PostMapping("/save")
+	@VersionControl("businessStockInventoryCheckServiceImpl")
+	public R save(@RequestBody BusinessStockInventoryCheckVO vo) {
+		return inventoryCheckService.save(vo);
+	}
+
+	/**
+	 * 提交(status=1,需有明细)
+	 */
+	@PostMapping("/submit")
+	@VersionControl("businessStockInventoryCheckServiceImpl")
+	public R submit(@RequestBody BusinessStockInventoryCheckVO vo) {
+		return inventoryCheckService.submit(vo);
+	}
+
+	/**
+	 * 撤销:已提交回退为保存态
+	 */
+	@PostMapping("/revoke")
+	@VersionControl("businessStockInventoryCheckServiceImpl")
+	public R revoke(@RequestBody BusinessStockInventoryCheckVO vo) {
+		return inventoryCheckService.revoke(vo);
+	}
+
+	/**
+	 * 删除:仅保存态允许逻辑删除(isDeleted=1)
+	 */
+	@PostMapping("/delete")
+	@VersionControl("businessStockInventoryCheckServiceImpl")
+	public R remove(@RequestBody BusinessStockInventoryCheckVO vo) {
+		return inventoryCheckService.remove(vo);
+	}
+}

+ 12 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckDetailMapper.java

@@ -0,0 +1,12 @@
+package com.trade.purchase.stock.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheckDetail;
+
+/**
+ * 库存盘点明细
+ *
+ * @author Rain
+ */
+public interface BusinessStockInventoryCheckDetailMapper extends BaseMapper<BusinessStockInventoryCheckDetail> {
+}

+ 9 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckDetailMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<!--suppress ALL -->
+<mapper namespace="com.trade.purchase.stock.mapper.BusinessStockInventoryCheckDetailMapper">
+
+
+
+
+</mapper>

+ 12 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckMapper.java

@@ -0,0 +1,12 @@
+package com.trade.purchase.stock.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheck;
+
+/**
+ * 库存盘点主表
+ *
+ * @author Rain
+ */
+public interface BusinessStockInventoryCheckMapper extends BaseMapper<BusinessStockInventoryCheck> {
+}

+ 8 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryCheckMapper.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<!--suppress ALL -->
+<mapper namespace="com.trade.purchase.stock.mapper.BusinessStockInventoryCheckMapper">
+
+
+
+</mapper>

+ 44 - 16
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/mapper/BusinessStockInventoryMapper.xml

@@ -42,22 +42,50 @@
     </resultMap>
 
     <select id="getStockBillFlowByInventoryId" resultMap="BusinessStockBillFlowResultMap">
-        select bsb.form_number,
-               case when bsb.bill_type = 0 then bsb.supplier_name else bsbd.supplier_name end as customer_or_supplier_name,
-               case when bsb.bill_type = 0 then '入库' else '出库' end as bill_type_name,
-               case when bsb.bill_type = 0 then bsbd.quantity else 0 end as increase_quantity,
-               case when bsb.bill_type = 1 then bsbd.quantity else 0 end as decrease_quantity,
-               bsb.bill_date
-        from business_stock_bill_detail bsbd
-                 inner join business_stock_bill bsb on bsb.id = bsbd.bill_id and bsb.is_deleted = 0
-                 inner join business_stock_inventory bsi on bsi.id = #{inventoryId}
-        where bsbd.is_deleted = 0
-          and bsb.status = 1
-          and bsbd.material_id = bsi.material_id
-          and (bsbd.warehouse_id = bsi.warehouse_id OR bsb.warehouse_id = bsi.warehouse_id)
-          and bsbd.storage_area_id = bsi.storage_area_id
-          and (bsbd.supplier_id = bsi.supplier_id OR bsb.supplier_id = bsi.supplier_id)
-        order by bsb.bill_date desc, bsbd.id desc
+        select flow.form_number,
+               flow.customer_or_supplier_name,
+               flow.bill_type_name,
+               flow.increase_quantity,
+               flow.decrease_quantity,
+               flow.bill_date
+        from (
+                 select bsb.form_number,
+                        case when bsb.bill_type = 0 then bsb.supplier_name else bsbd.supplier_name end as customer_or_supplier_name,
+                        case when bsb.bill_type = 0 then '入库' else '出库' end as bill_type_name,
+                        case when bsb.bill_type = 0 then bsbd.quantity else 0 end as increase_quantity,
+                        case when bsb.bill_type = 1 then bsbd.quantity else 0 end as decrease_quantity,
+                        bsb.bill_date,
+                        bsbd.id as flow_id
+                 from business_stock_bill_detail bsbd
+                          inner join business_stock_bill bsb on bsb.id = bsbd.bill_id and bsb.is_deleted = 0
+                          inner join business_stock_inventory bsi on bsi.id = #{inventoryId}
+                 where bsbd.is_deleted = 0
+                   and bsb.status = 1
+                   and bsbd.material_id = bsi.material_id
+                   and (bsbd.warehouse_id = bsi.warehouse_id OR bsb.warehouse_id = bsi.warehouse_id)
+                   and bsbd.storage_area_id = bsi.storage_area_id
+                   and (bsbd.supplier_id = bsi.supplier_id OR bsb.supplier_id = bsi.supplier_id)
+
+                 union all
+
+                 select bsic.form_number,
+                        bsicd.supplier_name as customer_or_supplier_name,
+                        '盘点' as bill_type_name,
+                        case when bsicd.variance_quantity > 0 then bsicd.variance_quantity else 0 end as increase_quantity,
+                        case when bsicd.variance_quantity &lt; 0 then -bsicd.variance_quantity else 0 end as decrease_quantity,
+                        bsic.check_date as bill_date,
+                        bsicd.id as flow_id
+                 from business_stock_inventory_check_detail bsicd
+                          inner join business_stock_inventory_check bsic on bsic.id = bsicd.check_id and bsic.is_deleted = 0
+                          inner join business_stock_inventory bsi on bsi.id = #{inventoryId}
+                 where bsicd.is_deleted = 0
+                   and bsic.status = 1
+                   and bsicd.material_id = bsi.material_id
+                   and bsicd.warehouse_id = bsi.warehouse_id
+                   and bsicd.storage_area_id = bsi.storage_area_id
+                   and bsicd.supplier_id = bsi.supplier_id
+             ) flow
+        order by flow.bill_date desc, flow.flow_id desc
     </select>
 
 </mapper>

+ 76 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/service/IBusinessStockInventoryCheckService.java

@@ -0,0 +1,76 @@
+package com.trade.purchase.stock.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.trade.purchase.stock.dto.BusinessStockInventoryCheckDTO;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheck;
+import com.trade.purchase.stock.vo.BusinessStockInventoryCheckVO;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.tool.api.R;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 库存盘点
+ *
+ * @author Rain
+ */
+public interface IBusinessStockInventoryCheckService extends IService<BusinessStockInventoryCheck> {
+
+	/**
+	 * 盘点分页列表
+	 *
+	 * @param vo    查询参数
+	 * @param query 分页参数
+	 * @return 分页结果
+	 */
+	IPage<BusinessStockInventoryCheckDTO> page(BusinessStockInventoryCheckVO vo, Query query);
+
+	/**
+	 * 导出盘点列表
+	 *
+	 * @param vo       查询参数
+	 * @param response 响应流
+	 */
+	void export(BusinessStockInventoryCheckVO vo, HttpServletResponse response);
+
+	/**
+	 * 盘点详情(含明细)
+	 *
+	 * @param id 盘点单ID
+	 * @return 详情数据
+	 */
+	BusinessStockInventoryCheckDTO detail(Long id);
+
+	/**
+	 * 保存盘点单(保存态)
+	 *
+	 * @param vo 参数
+	 * @return 结果
+	 */
+	R save(BusinessStockInventoryCheckVO vo);
+
+	/**
+	 * 提交盘点单
+	 *
+	 * @param vo 参数
+	 * @return 结果
+	 */
+	R submit(BusinessStockInventoryCheckVO vo);
+
+	/**
+	 * 撤销盘点单(已提交回退为保存)
+	 *
+	 * @param vo 参数
+	 * @return 结果
+	 */
+	R revoke(BusinessStockInventoryCheckVO vo);
+
+	/**
+	 * 删除盘点单(逻辑删除)
+	 *
+	 * @param vo 参数
+	 * @return 结果
+	 */
+	R remove(BusinessStockInventoryCheckVO vo);
+}

+ 0 - 24
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/service/impl/BusinessStockBillServiceImpl.java

@@ -487,15 +487,9 @@ public class BusinessStockBillServiceImpl extends ServiceImpl<BusinessStockBillM
 				updateInventory.setCurrentStock(newStock);
 				updateInventory.setTotalValue(totalValue);
 				updateInventory.setLastUpdatedDate(nowDate);
-				if (newStock > 0) {
-					updateInventory.setAverageCostPrice(totalValue.divide(BigDecimal.valueOf(newStock), 2, RoundingMode.DOWN));
-				} else {
-					updateInventory.setAverageCostPrice(BigDecimal.ZERO);
-				}
 				businessStockInventoryMapper.updateById(updateInventory);
 				inventory.setCurrentStock(newStock);
 				inventory.setTotalValue(totalValue);
-				inventory.setAverageCostPrice(updateInventory.getAverageCostPrice());
 			}
 		}
 
@@ -789,15 +783,9 @@ public class BusinessStockBillServiceImpl extends ServiceImpl<BusinessStockBillM
 				updateInventory.setCurrentStock(afterStock);
 				updateInventory.setTotalValue(afterTotalValue);
 				updateInventory.setLastUpdatedDate(nowDate);
-				if (afterStock > 0) {
-					updateInventory.setAverageCostPrice(afterTotalValue.divide(BigDecimal.valueOf(afterStock), 2, RoundingMode.DOWN));
-				} else {
-					updateInventory.setAverageCostPrice(BigDecimal.ZERO);
-				}
 				businessStockInventoryMapper.updateById(updateInventory);
 				sourceInventory.setCurrentStock(afterStock);
 				sourceInventory.setTotalValue(afterTotalValue);
-				sourceInventory.setAverageCostPrice(updateInventory.getAverageCostPrice());
 			}
 		}
 		if (CollectionUtil.isNotEmpty(shippedOrderItemIdList)) {
@@ -957,15 +945,9 @@ public class BusinessStockBillServiceImpl extends ServiceImpl<BusinessStockBillM
 				updateInventory.setCurrentStock(afterStock);
 				updateInventory.setTotalValue(afterTotalValue);
 				updateInventory.setLastUpdatedDate(nowDate);
-				if (afterStock > 0) {
-					updateInventory.setAverageCostPrice(afterTotalValue.divide(BigDecimal.valueOf(afterStock), 2, RoundingMode.DOWN));
-				} else {
-					updateInventory.setAverageCostPrice(BigDecimal.ZERO);
-				}
 				businessStockInventoryMapper.updateById(updateInventory);
 				sourceInventory.setCurrentStock(afterStock);
 				sourceInventory.setTotalValue(afterTotalValue);
-				sourceInventory.setAverageCostPrice(updateInventory.getAverageCostPrice());
 			}
 		}
 		return R.data(ResultCode.SUCCESS.getCode(), successItemIds, "生成出库单成功");
@@ -1064,15 +1046,9 @@ public class BusinessStockBillServiceImpl extends ServiceImpl<BusinessStockBillM
 			updateInventory.setCurrentStock(newStock);
 			updateInventory.setTotalValue(newTotalValue);
 			updateInventory.setLastUpdatedDate(nowDate);
-			if (newStock > 0) {
-				updateInventory.setAverageCostPrice(newTotalValue.divide(BigDecimal.valueOf(newStock), 2, RoundingMode.DOWN));
-			} else {
-				throw new RuntimeException("商品" + restoreLine.getMaterialName() + "库存不足,无法撤销");
-			}
 			businessStockInventoryMapper.updateById(updateInventory);
 			inventory.setCurrentStock(newStock);
 			inventory.setTotalValue(newTotalValue);
-			inventory.setAverageCostPrice(updateInventory.getAverageCostPrice());
 		}
 
 		BusinessStockBill updateBill = new BusinessStockBill();

+ 529 - 0
blade-service/trade-purchase/src/main/java/com/trade/purchase/stock/service/impl/BusinessStockInventoryCheckServiceImpl.java

@@ -0,0 +1,529 @@
+package com.trade.purchase.stock.service.impl;
+
+import cn.hutool.core.util.IdUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.trade.purchase.stock.dto.BusinessStockInventoryCheckDTO;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheck;
+import com.trade.purchase.stock.entity.BusinessStockInventoryCheckDetail;
+import com.trade.purchase.stock.entity.BusinessStockInventory;
+import com.trade.purchase.stock.excel.BusinessStockInventoryCheckExcel;
+import com.trade.purchase.stock.mapper.BusinessStockInventoryCheckDetailMapper;
+import com.trade.purchase.stock.mapper.BusinessStockInventoryCheckMapper;
+import com.trade.purchase.stock.mapper.BusinessStockInventoryMapper;
+import lombok.Data;
+import com.trade.purchase.stock.service.IBusinessStockInventoryCheckService;
+import com.trade.purchase.stock.utils.BillCodeUtil;
+import com.trade.purchase.stock.vo.BusinessStockInventoryCheckVO;
+import lombok.AllArgsConstructor;
+import org.springblade.core.excel.util.ExcelUtil;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.BladeUser;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.api.ResultCode;
+import org.springblade.core.tool.utils.BeanUtil;
+import org.springblade.core.tool.utils.CollectionUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * 库存盘点
+ *
+ * @author Rain
+ */
+@Service
+@AllArgsConstructor
+public class BusinessStockInventoryCheckServiceImpl extends ServiceImpl<BusinessStockInventoryCheckMapper, BusinessStockInventoryCheck> implements IBusinessStockInventoryCheckService {
+
+	private final BusinessStockInventoryCheckMapper checkMapper;
+	private final BusinessStockInventoryCheckDetailMapper detailMapper;
+	private final BusinessStockInventoryMapper inventoryMapper;
+
+	@Override
+	public IPage<BusinessStockInventoryCheckDTO> page(BusinessStockInventoryCheckVO vo, Query query) {
+		if (Objects.isNull(vo)) {
+			vo = new BusinessStockInventoryCheckVO();
+		}
+		BladeUser user = AuthUtil.getUser();
+		LambdaQueryWrapper<BusinessStockInventoryCheck> wrapper = buildListWrapper(user.getTenantId(), vo);
+		IPage<BusinessStockInventoryCheck> entityPage = checkMapper.selectPage(Condition.getPage(query), wrapper);
+		return entityPage.convert(row -> BeanUtil.copy(row, BusinessStockInventoryCheckDTO.class));
+	}
+
+	@Override
+	public void export(BusinessStockInventoryCheckVO vo, HttpServletResponse response) {
+		if (Objects.isNull(vo)) {
+			vo = new BusinessStockInventoryCheckVO();
+		}
+		BladeUser user = AuthUtil.getUser();
+		List<BusinessStockInventoryCheck> list = checkMapper.selectList(buildListWrapper(user.getTenantId(), vo));
+		List<BusinessStockInventoryCheckExcel> excelRows = new ArrayList<>();
+		if (CollectionUtil.isNotEmpty(list)) {
+			List<Long> checkIdList = list.stream().map(BusinessStockInventoryCheck::getId).filter(Objects::nonNull).collect(Collectors.toList());
+			Map<Long, List<BusinessStockInventoryCheckDetail>> detailMap = new LinkedHashMap<>();
+			if (CollectionUtil.isNotEmpty(checkIdList)) {
+				List<BusinessStockInventoryCheckDetail> details = detailMapper.selectList(new LambdaQueryWrapper<BusinessStockInventoryCheckDetail>()
+					.in(BusinessStockInventoryCheckDetail::getCheckId, checkIdList)
+					.eq(BusinessStockInventoryCheckDetail::getTenantId, user.getTenantId())
+					.eq(BusinessStockInventoryCheckDetail::getIsDeleted, 0)
+					.orderByAsc(BusinessStockInventoryCheckDetail::getSort)
+					.orderByAsc(BusinessStockInventoryCheckDetail::getId));
+				detailMap = details.stream().collect(Collectors.groupingBy(BusinessStockInventoryCheckDetail::getCheckId, LinkedHashMap::new, Collectors.toList()));
+			}
+			for (BusinessStockInventoryCheck row : list) {
+				List<BusinessStockInventoryCheckDetail> detailList = detailMap.get(row.getId());
+				if (CollectionUtil.isEmpty(detailList)) {
+					excelRows.add(buildExportRow(row, null));
+					continue;
+				}
+				for (BusinessStockInventoryCheckDetail detail : detailList) {
+					excelRows.add(buildExportRow(row, detail));
+				}
+			}
+		}
+		ExcelUtil.export(response, "库存盘点", "库存盘点", excelRows, BusinessStockInventoryCheckExcel.class);
+	}
+
+	private BusinessStockInventoryCheckExcel buildExportRow(BusinessStockInventoryCheck head, BusinessStockInventoryCheckDetail detail) {
+		BusinessStockInventoryCheckExcel ex = new BusinessStockInventoryCheckExcel();
+		ex.setFormNumber(head.getFormNumber());
+		ex.setWarehouse(head.getWarehouse());
+		ex.setCheckDate(head.getCheckDate());
+		ex.setStatusLabel(resolveStatusLabel(head.getStatus()));
+		ex.setTotalVarianceQty(head.getTotalVarianceQty());
+		ex.setTotalVarianceAmount(head.getTotalVarianceAmount());
+		ex.setRemarks(head.getRemarks());
+		ex.setCreator(head.getCreator());
+		ex.setCreatedAt(head.getCreatedAt());
+		if (Objects.nonNull(detail)) {
+			ex.setMaterialCode(detail.getMaterialCode());
+			ex.setMaterialName(detail.getMaterialName());
+			ex.setSupplierName(detail.getSupplierName());
+			ex.setStorageArea(detail.getStorageArea());
+			ex.setBookQuantity(detail.getBookQuantity());
+			ex.setActualQuantity(detail.getActualQuantity());
+			ex.setVarianceQuantity(detail.getVarianceQuantity());
+			ex.setVarianceAmount(detail.getVarianceAmount());
+			ex.setNote(detail.getNote());
+		}
+		return ex;
+	}
+
+	@Override
+	public BusinessStockInventoryCheckDTO detail(Long id) {
+		if (Objects.isNull(id)) {
+			return null;
+		}
+		BladeUser user = AuthUtil.getUser();
+		BusinessStockInventoryCheck head = checkMapper.selectOne(new LambdaQueryWrapper<BusinessStockInventoryCheck>()
+			.eq(BusinessStockInventoryCheck::getId, id)
+			.eq(BusinessStockInventoryCheck::getTenantId, user.getTenantId())
+			.eq(BusinessStockInventoryCheck::getIsDeleted, 0)
+			.last("limit 1"));
+		if (Objects.isNull(head)) {
+			return null;
+		}
+		BusinessStockInventoryCheckDTO dto = BeanUtil.copy(head, BusinessStockInventoryCheckDTO.class);
+		if (Objects.isNull(dto)) {
+			return null;
+		}
+		List<BusinessStockInventoryCheckDetail> details = detailMapper.selectList(new LambdaQueryWrapper<BusinessStockInventoryCheckDetail>()
+			.eq(BusinessStockInventoryCheckDetail::getCheckId, id)
+			.eq(BusinessStockInventoryCheckDetail::getIsDeleted, 0)
+			.orderByAsc(BusinessStockInventoryCheckDetail::getSort)
+			.orderByAsc(BusinessStockInventoryCheckDetail::getId));
+		dto.setDetailList(details);
+		return dto;
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R save(BusinessStockInventoryCheckVO vo) {
+		return persist(vo, 0, false);
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R submit(BusinessStockInventoryCheckVO vo) {
+		return persist(vo, 1, true);
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R revoke(BusinessStockInventoryCheckVO vo) {
+		if (Objects.isNull(vo) || Objects.isNull(vo.getId())) {
+			return R.fail("参数错误");
+		}
+		BladeUser user = AuthUtil.getUser();
+		Date now = new Date();
+		BusinessStockInventoryCheck head = checkMapper.selectOne(new LambdaQueryWrapper<BusinessStockInventoryCheck>()
+			.eq(BusinessStockInventoryCheck::getId, vo.getId())
+			.eq(BusinessStockInventoryCheck::getTenantId, user.getTenantId())
+			.eq(BusinessStockInventoryCheck::getIsDeleted, 0)
+			.last("limit 1"));
+		if (Objects.isNull(head)) {
+			return R.fail("单据不存在");
+		}
+		if (!Objects.equals(head.getStatus(), 1)) {
+			return R.fail("仅已提交的单据可撤销为保存");
+		}
+		Date submitTime = Optional.ofNullable(head.getUpdatedAt()).orElse(head.getCreatedAt());
+		if (Objects.isNull(submitTime)) {
+			return R.fail("提交时间异常,无法撤销");
+		}
+		long nowMillis = now.getTime();
+		long submitMillis = submitTime.getTime();
+		if (nowMillis - submitMillis > 86400000L) {
+			return R.fail("提交超过一天,不允许撤销");
+		}
+		BusinessStockInventoryCheck update = new BusinessStockInventoryCheck();
+		update.setId(head.getId());
+		update.setStatus(0);
+		update.setUpdateUserId(user.getUserId());
+		update.setUpdatedAt(now);
+		checkMapper.updateById(update);
+		return R.success("撤销成功");
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public R remove(BusinessStockInventoryCheckVO vo) {
+		if (Objects.isNull(vo) || Objects.isNull(vo.getId())) {
+			return R.fail("参数错误");
+		}
+		BladeUser user = AuthUtil.getUser();
+		Date now = new Date();
+		BusinessStockInventoryCheck head = checkMapper.selectOne(new LambdaQueryWrapper<BusinessStockInventoryCheck>()
+			.eq(BusinessStockInventoryCheck::getId, vo.getId())
+			.eq(BusinessStockInventoryCheck::getTenantId, user.getTenantId())
+			.eq(BusinessStockInventoryCheck::getIsDeleted, 0)
+			.last("limit 1"));
+		if (Objects.isNull(head)) {
+			return R.fail("单据不存在");
+		}
+		if (!Objects.equals(head.getStatus(), 0)) {
+			return R.fail("仅保存状态单据允许删除");
+		}
+		BusinessStockInventoryCheck update = new BusinessStockInventoryCheck();
+		update.setId(head.getId());
+		update.setIsDeleted(1);
+		update.setUpdateUserId(user.getUserId());
+		update.setUpdatedAt(now);
+		checkMapper.updateById(update);
+
+		BusinessStockInventoryCheckDetail removeDetail = new BusinessStockInventoryCheckDetail();
+		removeDetail.setIsDeleted(1);
+		removeDetail.setUpdatedTime(now);
+		detailMapper.update(removeDetail, new LambdaQueryWrapper<BusinessStockInventoryCheckDetail>()
+			.eq(BusinessStockInventoryCheckDetail::getCheckId, head.getId())
+			.eq(BusinessStockInventoryCheckDetail::getTenantId, user.getTenantId())
+			.eq(BusinessStockInventoryCheckDetail::getIsDeleted, 0));
+		return R.success("删除成功");
+	}
+
+	private R persist(BusinessStockInventoryCheckVO vo, int targetStatus, boolean requireDetails) {
+		if (Objects.isNull(vo)) {
+			return R.fail("参数错误");
+		}
+		BladeUser user = AuthUtil.getUser();
+		Date now = new Date();
+		List<BusinessStockInventoryCheckDetail> detailList = Optional.ofNullable(vo.getDetailList()).orElseGet(ArrayList::new);
+		List<BusinessStockInventoryCheckDetail> delDetailList = Optional.ofNullable(vo.getDelDetailList()).orElseGet(ArrayList::new);
+
+		if (requireDetails && CollectionUtil.isEmpty(detailList)) {
+			return R.fail("请添加盘点明细");
+		}
+
+		BusinessStockInventoryCheck head = BeanUtil.copy(vo, BusinessStockInventoryCheck.class);
+		if (Objects.isNull(head)) {
+			return R.fail("参数错误");
+		}
+		head.setStatus(targetStatus);
+		if (Objects.isNull(head.getCheckDate())) {
+			head.setCheckDate(now);
+		}
+
+		for (BusinessStockInventoryCheckDetail d : detailList) {
+			fillDetailLine(d);
+		}
+
+		if (Objects.isNull(head.getId())) {
+			applyHeaderTotals(head, detailList);
+			head.setCreator(user.getUserName());
+			head.setCreateUserId(user.getUserId());
+			head.setCreatedAt(now);
+			head.setFormNumber(BillCodeUtil.getBillCodeByType(user.getTenantId(), "PD"));
+			head.setTenantId(user.getTenantId());
+			head.setIsDeleted(0);
+			head.setCreatedDate(now);
+			checkMapper.insert(head);
+		} else {
+			BusinessStockInventoryCheck existed = checkMapper.selectOne(new LambdaQueryWrapper<BusinessStockInventoryCheck>()
+				.eq(BusinessStockInventoryCheck::getId, head.getId())
+				.eq(BusinessStockInventoryCheck::getTenantId, user.getTenantId())
+				.eq(BusinessStockInventoryCheck::getIsDeleted, 0)
+				.last("limit 1"));
+			if (Objects.isNull(existed)) {
+				return R.fail("单据不存在");
+			}
+			if (Objects.equals(existed.getStatus(), 1) && targetStatus == 0) {
+				return R.fail("已提交单据请使用撤销接口回退");
+			}
+			if (Objects.equals(existed.getStatus(), 1) && targetStatus == 1) {
+				return R.fail("单据已提交,请先撤销后再修改");
+			}
+			mergeCheckHead(existed, head);
+			existed.setStatus(targetStatus);
+			applyHeaderTotals(existed, detailList);
+			existed.setUpdateUserId(user.getUserId());
+			existed.setUpdatedAt(now);
+			checkMapper.updateById(existed);
+			head = existed;
+		}
+
+		Long checkId = head.getId();
+		for (BusinessStockInventoryCheckDetail d : detailList) {
+			d.setCheckId(checkId);
+			d.setTenantId(user.getTenantId());
+			d.setIsDeleted(0);
+			d.setUpdatedTime(now);
+		}
+
+		List<Long> delIds = delDetailList.stream().map(BusinessStockInventoryCheckDetail::getId).filter(Objects::nonNull).collect(Collectors.toList());
+		if (!delIds.isEmpty()) {
+			BusinessStockInventoryCheckDetail deleteItem = new BusinessStockInventoryCheckDetail();
+			deleteItem.setIsDeleted(1);
+			deleteItem.setUpdatedTime(now);
+			detailMapper.update(deleteItem, new LambdaQueryWrapper<BusinessStockInventoryCheckDetail>()
+				.eq(BusinessStockInventoryCheckDetail::getCheckId, checkId)
+				.in(BusinessStockInventoryCheckDetail::getId, delIds));
+		}
+
+		if (Objects.isNull(vo.getId())) {
+			for (BusinessStockInventoryCheckDetail d : detailList) {
+				if (Objects.isNull(d.getId())) {
+					d.setId(IdUtil.getSnowflakeNextId());
+					d.setCreatedTime(now);
+				}
+				detailMapper.insert(d);
+			}
+		} else {
+			List<BusinessStockInventoryCheckDetail> toUpdate = detailList.stream().filter(d -> Objects.nonNull(d.getId())).collect(Collectors.toList());
+			List<BusinessStockInventoryCheckDetail> toInsert = detailList.stream().filter(d -> Objects.isNull(d.getId())).collect(Collectors.toList());
+			for (BusinessStockInventoryCheckDetail d : toUpdate) {
+				detailMapper.updateById(d);
+			}
+			for (BusinessStockInventoryCheckDetail d : toInsert) {
+				d.setId(IdUtil.getSnowflakeNextId());
+				d.setCreatedTime(now);
+				detailMapper.insert(d);
+			}
+		}
+
+		if (targetStatus == 1) {
+			applyInventoryByCheckResult(checkId, user.getTenantId(), now);
+		}
+
+		String msg = targetStatus == 1 ? "提交成功" : "保存成功";
+		return R.data(ResultCode.SUCCESS.getCode(), checkId, msg);
+	}
+
+	/**
+	 * 提交盘点单后,按差异量调整库存数量与金额。
+	 */
+	private void applyInventoryByCheckResult(Long checkId, String tenantId, Date now) {
+		List<BusinessStockInventoryCheckDetail> details = detailMapper.selectList(new LambdaQueryWrapper<BusinessStockInventoryCheckDetail>()
+			.eq(BusinessStockInventoryCheckDetail::getCheckId, checkId)
+			.eq(BusinessStockInventoryCheckDetail::getTenantId, tenantId)
+			.eq(BusinessStockInventoryCheckDetail::getIsDeleted, 0));
+		if (CollectionUtil.isEmpty(details)) {
+			throw new RuntimeException("盘点明细不存在,无法提交");
+		}
+
+		Map<String, InventoryAdjustLine> adjustLineMap = new LinkedHashMap<>();
+		for (BusinessStockInventoryCheckDetail d : details) {
+			if (Objects.isNull(d.getMaterialId())) {
+				continue;
+			}
+			int deltaQty = Optional.ofNullable(d.getVarianceQuantity()).orElse(0);
+			BigDecimal deltaAmount = Optional.ofNullable(d.getVarianceAmount()).orElse(BigDecimal.ZERO);
+			if (deltaQty == 0 && deltaAmount.compareTo(BigDecimal.ZERO) == 0) {
+				continue;
+			}
+			String key = buildInventoryAdjustKey(d.getMaterialId(), d.getWarehouseId(), d.getStorageAreaId(), d.getSupplierId());
+			InventoryAdjustLine line = adjustLineMap.get(key);
+			if (Objects.isNull(line)) {
+				line = new InventoryAdjustLine();
+				line.setKey(key);
+				line.setMaterialId(d.getMaterialId());
+				line.setWarehouseId(d.getWarehouseId());
+				line.setStorageAreaId(d.getStorageAreaId());
+				line.setSupplierId(d.getSupplierId());
+				line.setMaterialName(d.getMaterialName());
+				line.setDeltaQty(0);
+				line.setDeltaAmount(BigDecimal.ZERO);
+				adjustLineMap.put(key, line);
+			}
+			line.setDeltaQty(line.getDeltaQty() + deltaQty);
+			line.setDeltaAmount(line.getDeltaAmount().add(deltaAmount));
+		}
+		if (CollectionUtil.isEmpty(adjustLineMap)) {
+			return;
+		}
+
+		List<Long> materialIds = adjustLineMap.values().stream()
+			.map(InventoryAdjustLine::getMaterialId)
+			.filter(Objects::nonNull)
+			.distinct()
+			.collect(Collectors.toList());
+		List<BusinessStockInventory> inventoryList = inventoryMapper.selectList(new LambdaQueryWrapper<BusinessStockInventory>()
+			.eq(BusinessStockInventory::getTenantId, tenantId)
+			.eq(BusinessStockInventory::getIsActive, 1)
+			.in(BusinessStockInventory::getMaterialId, materialIds));
+		Map<String, BusinessStockInventory> inventoryMap = inventoryList.stream()
+			.collect(Collectors.toMap(this::buildInventoryAdjustKey, inv -> inv, (a, b) -> a));
+
+		for (InventoryAdjustLine line : adjustLineMap.values()) {
+			BusinessStockInventory inventory = inventoryMap.get(line.getKey());
+			if (Objects.isNull(inventory)) {
+				throw new RuntimeException("商品" + line.getMaterialName() + "未找到对应库存,无法提交");
+			}
+			int stockBefore = Optional.ofNullable(inventory.getCurrentStock()).orElse(0);
+			BigDecimal amountBefore = Optional.ofNullable(inventory.getTotalValue()).orElse(BigDecimal.ZERO);
+			int stockAfter = stockBefore + line.getDeltaQty();
+			if (stockAfter < 0) {
+				throw new RuntimeException("商品" + line.getMaterialName() + "盘点后库存不能小于0");
+			}
+			BigDecimal amountAfter = amountBefore.add(line.getDeltaAmount());
+			if (amountAfter.compareTo(BigDecimal.ZERO) < 0) {
+				amountAfter = BigDecimal.ZERO;
+			}
+			BusinessStockInventory update = new BusinessStockInventory();
+			update.setId(inventory.getId());
+			update.setCurrentStock(stockAfter);
+			update.setTotalValue(amountAfter);
+			update.setLastUpdatedDate(now);
+			if (stockAfter > 0) {
+				update.setAverageCostPrice(amountAfter.divide(BigDecimal.valueOf(stockAfter), 2, RoundingMode.DOWN));
+			} else {
+				update.setAverageCostPrice(BigDecimal.ZERO);
+			}
+			inventoryMapper.updateById(update);
+		}
+	}
+
+	private String buildInventoryAdjustKey(BusinessStockInventory inventory) {
+		return buildInventoryAdjustKey(inventory.getMaterialId(), inventory.getWarehouseId(), inventory.getStorageAreaId(), inventory.getSupplierId());
+	}
+
+	private String buildInventoryAdjustKey(Long materialId, Long warehouseId, Long storageAreaId, Long supplierId) {
+		return String.valueOf(materialId) + "_" + String.valueOf(warehouseId) + "_" + String.valueOf(storageAreaId) + "_" + String.valueOf(supplierId);
+	}
+
+	@Data
+	private static final class InventoryAdjustLine {
+		private String key;
+		private Long materialId;
+		private Long warehouseId;
+		private Long storageAreaId;
+		private Long supplierId;
+		private String materialName;
+		private Integer deltaQty;
+		private BigDecimal deltaAmount;
+	}
+
+	private static void fillDetailLine(BusinessStockInventoryCheckDetail d) {
+		Integer book = d.getBookQuantity();
+		Integer actual = d.getActualQuantity();
+		if (book != null && actual != null) {
+			d.setVarianceQuantity(actual - book);
+		}
+		BigDecimal bookAmt = d.getBookAmount();
+		BigDecimal actAmt = d.getActualAmount();
+		if (bookAmt != null && actAmt != null) {
+			d.setVarianceAmount(actAmt.subtract(bookAmt));
+		} else if (d.getVarianceQuantity() != null && d.getAverageCostPrice() != null) {
+			d.setVarianceAmount(d.getAverageCostPrice().multiply(BigDecimal.valueOf(d.getVarianceQuantity())));
+		}
+	}
+
+	private static void applyHeaderTotals(BusinessStockInventoryCheck head, List<BusinessStockInventoryCheckDetail> details) {
+		if (CollectionUtil.isEmpty(details)) {
+			head.setTotalVarianceQty(0);
+			head.setTotalVarianceAmount(BigDecimal.ZERO);
+			return;
+		}
+		int sumQty = 0;
+		BigDecimal sumAmt = BigDecimal.ZERO;
+		for (BusinessStockInventoryCheckDetail d : details) {
+			if (d.getVarianceQuantity() != null) {
+				sumQty += d.getVarianceQuantity();
+			}
+			if (d.getVarianceAmount() != null) {
+				sumAmt = sumAmt.add(d.getVarianceAmount());
+			}
+		}
+		head.setTotalVarianceQty(sumQty);
+		head.setTotalVarianceAmount(sumAmt);
+	}
+
+	private LambdaQueryWrapper<BusinessStockInventoryCheck> buildListWrapper(String tenantId, BusinessStockInventoryCheckVO vo) {
+		LambdaQueryWrapper<BusinessStockInventoryCheck> w = new LambdaQueryWrapper<>();
+		w.eq(BusinessStockInventoryCheck::getTenantId, tenantId);
+		w.eq(BusinessStockInventoryCheck::getIsDeleted, 0);
+		w.like(StringUtil.isNotBlank(vo.getFormNumber()), BusinessStockInventoryCheck::getFormNumber, vo.getFormNumber());
+		w.eq(Objects.nonNull(vo.getWarehouseId()), BusinessStockInventoryCheck::getWarehouseId, vo.getWarehouseId());
+		w.eq(Objects.nonNull(vo.getStatus()), BusinessStockInventoryCheck::getStatus, vo.getStatus());
+		w.ge(StringUtil.isNotBlank(vo.getCheckDateStart()), BusinessStockInventoryCheck::getCheckDate, vo.getCheckDateStart());
+		w.le(StringUtil.isNotBlank(vo.getCheckDateEnd()), BusinessStockInventoryCheck::getCheckDate, vo.getCheckDateEnd());
+		w.orderByDesc(BusinessStockInventoryCheck::getCreatedAt);
+		return w;
+	}
+
+	private static String resolveStatusLabel(Integer status) {
+		if (Objects.equals(status, 1)) {
+			return "已提交";
+		}
+		return "保存";
+	}
+
+	/**
+	 * 更新时合并字段,避免前端未传字段被置空。
+	 */
+	private static void mergeCheckHead(BusinessStockInventoryCheck target, BusinessStockInventoryCheck patch) {
+		if (Objects.isNull(patch)) {
+			return;
+		}
+		if (Objects.nonNull(patch.getWarehouseId())) {
+			target.setWarehouseId(patch.getWarehouseId());
+		}
+		if (Objects.nonNull(patch.getWarehouse())) {
+			target.setWarehouse(patch.getWarehouse());
+		}
+		if (Objects.nonNull(patch.getCheckDate())) {
+			target.setCheckDate(patch.getCheckDate());
+		}
+		if (Objects.nonNull(patch.getRemarks())) {
+			target.setRemarks(patch.getRemarks());
+		}
+		if (Objects.nonNull(patch.getCreatedDate())) {
+			target.setCreatedDate(patch.getCreatedDate());
+		}
+	}
+}